API

This API reference is automatically generated from the docstrings found within the source code. It’s meant as an easy to use and easy to share window into the code base.

Take a look around! The code is simple and short.

textsmith.constants

Constant values for textual worlds. Mostly indicates default names for object attributes.

Copyright (C) 2020 Nicholas H.Tollervey (ntoll@ntoll.org).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>

textsmith.constants.ALIAS = '.alias'

The attribute containing a list of the object’s aliases.

textsmith.constants.DESCRIPTION = 'description'

The attribute giving the full description of an object.

textsmith.constants.DESTINATION = 'destination'

The attribute to indicate the destination of an exit.

textsmith.constants.EMIT = 'emit'

The attribute describing how an object emits output.

textsmith.constants.EMOTE = 'emote'

The attribute describing how an object emotes.

textsmith.constants.ENTER_ROOM = 'enter_room'

The attribute describing a user’s entrance to a room.

textsmith.constants.EXIT_ROOM = 'exit_room'

The attribute describing a user’s exit from a room.

textsmith.constants.HUH = [l'Huh? That doesn't make sense to me.', l'I don't understand that.', l'Nope. No idea what you're on about.', l'I don't know what you mean.', l'Try explaining that in a way I can understand.', l'Yeah right... as if I know what you're on about. :-)', l'Let me try tha... nope.', l'Ummm... you're not making sense. Again, but with feeling!', l'No idea. Try giving me something I understand.', l'Huh? I don't understand. Maybe ask someone for help?', l'Try using commands I understand.']

Default messages of last resort when the user’s input cannot be parsed. Huh?

textsmith.constants.IS_DELETED = '.deleted'

The attribute flagging that an object is deleted.

textsmith.constants.IS_EXIT = '.exit'

The attribute to indicate if an object is an exit.

textsmith.constants.IS_ROOM = '.room'

The attribute to indicate the object is a room.

textsmith.constants.IS_SCRIPT = '#!'

Default indicator at the start of a string to indicate it is a script.

textsmith.constants.IS_USER = '.user'

The attribute to indicate an object is a user.

textsmith.constants.MATCH_OBJECT_ID = re.compile('^#\\d+$')

Regex for matching object ids. e.g. #1234.

textsmith.constants.MOVABLE = 'movable'

The attribute to flag if an object can be moved.

textsmith.constants.NAME = 'name'

The attribute containing the object’s primary name.

textsmith.constants.OWNER = '.owner'

The attribute to indicate an object’s owner.

textsmith.constants.ROOM_ALIASES = [l'here', l'hither']

Default aliases for the current location.

textsmith.constants.SAYS = 'say'

The attribute describing how an object says.

textsmith.constants.SHOUTS = 'shout'

The attribute describing how an object shouts.

textsmith.constants.SUMMARY = 'summary'

The attribute containing the summary (short) description of an object.

textsmith.constants.SYSTEM_OUTPUT = '<pre><code>{}</code></pre>'

The HTML fragment template for system or error messages for the user.

textsmith.constants.TELL = 'tell'

The attribute describing how an object tells.

textsmith.constants.TRAVEL = 'travel'

The attribute describing movement through an exit.

textsmith.constants.USER_ALIASES = [l'me', l'myself']

Default aliases for the current user.

textsmith.datastore

Functions that CRUD state stored in a Redis datastore. Data for objects is stored in Redis Hashes whose values are serialized as strings of JSON.

Copyright (C) 2020 Nicholas H.Tollervey (ntoll@ntoll.org).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>

class textsmith.datastore.DataStore(redis: asyncio_redis.pool.Pool)[source]

Gathers together methods to implement storage related operations via Redis.

add_object(**attributes) → int[source]

Create a new object. The new object’s parent object is referenced by parent_id.

annotate_object(object_id: int, **attributes) → None[source]

Annotate attributes to the object.

confirm_user(confirmation_token: str, password: str) → str[source]

Given a confirmation token sets the referenced password against the email address related to the token. This is the final step in user confirmation.

create_user(email: str, confirmation_token: str) → int[source]

Create metadata for the new user identified by the referenced email address and using the referenced password. Return the id of the object in the database associated with this user.

delete_attributes(object_id: int, attributes: Sequence[str]) → None[source]

Given an object ID and list of attributes, delete them. Returns the number of attributes deleted.

delete_object(object_id: int) → None[source]

Soft delete an object from the database. This involves setting the is_deleted flag and ensuring the object isn’t contained within another object. The current time is set for the deleted flag.

delete_user(email: str) → None[source]

Soft delete the user whilst keeping all the objects owned by the user (who is identified by the referenced email address). This involves setting the user as inactive (so they can’t log in) and ensuring they are not contained within another object.

email_to_object_id(email: str) → int[source]

Return the id of the in game object representing the player identified by the referenced email address.

get_attribute(object_id: int, attribute: str) → Union[str, int, float, bool, Sequence[Union[str, int, float, bool]]][source]

Given an object ID and attribute, return the associated value or raise a KeyError to indicate the attribute doesn’t exist on the object.

get_contents(object_id: int) → Dict[int, Dict[str, Union[str, int, float, bool, Sequence[Union[str, int, float, bool]]]]][source]

Return a dictionary containing all the objects contained within the referenced object.

get_last_seen(user_id: int) → Optional[datetime.datetime][source]

Returns a datetime object representing the moment at which the user, whose in-game object is referenced in the arguments, was last seen.

get_location(object_id) → Optional[int][source]

Given an object_id, return the id of the object that contains it. If the object is not contained within another object, return None.

get_objects(ids: Sequence[int]) → Dict[int, Dict[str, Union[str, int, float, bool, Sequence[Union[str, int, float, bool]]]]][source]

Given a list of object IDs, return a dictionary whose keys are object IDs and values are a dictionary of the related attributes of each object.

get_script_context(user_id: int) → Dict[KT, VT][source]

Returns a complete context in order that a script can be executed.

get_user_context(user_id: int) → Dict[str, Any][source]

Return the user object and a representation of the object containing the user. This is used to obtain the minimal context needed for social interactions (saying, emoting etc):

{
  "user": { .. object representing the user .. },
  "room": { .. object representing the room .. },
}
get_users_in_room(object_id: int) → Sequence[Dict[str, Union[str, int, float, bool, Sequence[Union[str, int, float, bool]]]]][source]

Return a list of object ids for users who are contained within the room identified by the object id passed into the method.

hash_password(password: str) → str[source]

Hash a password for safe storage.

inventory_key(object_id: int) → str[source]

Given an object id, return the key to use to record the objects contained within the referenced object. This is recording “what do I contain?”

last_seen_key(user_id: int) → str[source]

Given a user id, return the key to use to set the timestamp at which the user was last seen on the server.

location_key(object_id: int) → str[source]

Given an object id, return the key used to record the id of the object that contains the referenced object. This is recording “who contains me?”

set_container(object_id: int, container_id: int) → None[source]

Ensure the referenced object is set to be contained by the object referenced as container_id. If the container_id < 0, then the referenced object_id is not contained anywhere.

set_last_seen(email: str) → None[source]

Set the last_seen value for the user identified by the referenced object id.

set_user_active(email: str, active_flag: bool = True) → None[source]

Set the “active” flag against the user identified via the email address to the value of “active_flag”.

set_user_password(email: str, password: str) → bool[source]

Given a user identified by the referenced email address, update their password to the one provided as an argument to this function.

Passwords cannot be set for non-existent users, nor inactive users.

token_key(token: str) → str[source]

Given a token value, return the key to use to retrieve the associated user’s details.

token_to_email(confirmation_token: str) → Optional[str][source]

Given a confirmation token, will return the related email address. If no email or token exists, returns None.

user_exists(email: str) → bool[source]

Returns a boolean indication if a user linked to the referenced email address exists within the system.

user_key(email: str) → str[source]

Given a user’s unique email address, return the key to use to reference the user in the Redis database.

verify_password(stored_password: str, provided_password: str) → bool[source]

Verify a stored password hash against a plaintext provided password.

verify_user(email: str, password: str) → bool[source]

Given an email address and password, will check that the credentials are valid for signing into the system.

textsmith.logic

Functions that implement application logic.

Copyright (C) 2020 Nicholas H.Tollervey (ntoll@ntoll.org).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>

class textsmith.logic.Logic(datastore: textsmith.datastore.DataStore, email_host: str, email_port: int, email_from: str, email_password: str)[source]

Gathers together methods which implement application logic. Uses the dependency injection pattern.

check_email(email: str) → bool[source]

Return a boolean indication if an email address is not already taken.

check_token(confirmation_token: str) → Optional[str][source]

Return the email address of the user associated with the token, or None if it doesn’t exist.

clarify_object(user_id: int, message: str, match: Sequence[Dict[KT, VT]]) → None[source]

A problem match (containing more than one object) needs to be handled by asking the user to clarify or re-state their term of reference to the desired object.

confirm_user(confirmation_token: str, password: str) → None[source]

Given the user has followed the link containing the confirmation token and successfully set a valid password: update their record, activate them and send them a welcome email.

create_user(email: str) → None[source]

Create a user with the referenced email. Email a confirmation link with instructions for setting up a password to the new user.

emit_to_room(room_id: int, exclude: Sequence[int], message: str) → None[source]

Emit a message to all users not in the exclude list in the referenced room.

emit_to_user(user_id: int, message: str) → None[source]

Emit a message to the referenced user. All messages are run through Markdown.

get_attribute_value(obj: Dict[KT, VT], attribute: str) → str[source]

Return the value of the referenced object attribute. If the value is a string that starts with constants.IS_SCRIPT evaluate it and return the result. Otherwise, return a string representation of the value. If there is no such value, return an empty string.

get_script_context(user_id: int, connection_id: str, message_id: str) → Dict[KT, VT][source]

Return a dictionary representation of the room-wide context in which the user finds themselves:

{
    "user": { ... user's attributes ... },
    "room": { ... room's attributes ... },
    "exits": [{ ... exits from the room ... }, ],
    "users": [{ ... other users in the room ...}, ],
    "things": [{ ... other objects in the room ...}, ],
}
get_user_context(user_id: int, connection_id: str, message_id: str) → Dict[KT, VT][source]

Return a dictionary representation of the immediate context in which the user finds themselves:

{
    "user": { ... user's attributes ... },
    "room": { ... room's attributes ... },
}
match_object(identifier: str, context: Dict[KT, VT]) → Tuple[Sequence[Dict[KT, VT]], str][source]

Given a potentially ambiguous user entered identifier, try to find a matching object in the given context.

An object’s name, object id or alias is assumed to begin the identifier string. The identifier string is always normalised: it is stripped of leading and trailing whitespace and matches are case insensitive.

An object id is an integer starting with “#”. For example, #123.

A name or alias may be a multi-word reference to the object.

A match will be the shortest sequence of words that also match the id, name or aliases of those objects that are the current user, the current room, exits from the current room, other users within the current room and things found in the current room all in the current context.

The special aliases found in constants.USER_ALIASES always refer to the current user, and aliases found in constants.ROOM_ALIASES refer to the current room.

This method returns two values: a list of matching objects (or an empty list of no matches found), and a string representing the token that made the match (or an empty string if there were no matches).

For example, given the identifier: “#378 some more text”, the return values will be a list containing a single dictionary representing the object with the id 378, and a string “#378” to indicate it was “#378” that caused the match.

matches_name(name: str, obj: Dict[KT, VT]) → bool[source]

Returns a boolean indication if the referenced object matches the given name. This is case insensitive and checks the name and alias list for a name match.

no_matching_object(user_id: int, message: str) → None[source]

There is a problem because the expected object match could not be found. Report this and ask the user to clarify or re-state their term of reference to the desired object.

send_email(message: email.message.EmailMessage) → None[source]

Asynchronously log and send the referenced email.message.EmailMessage.

set_last_seen(email: str) → None[source]

Set the last_seen timestamp to time.now() for the user with the referenced email address.

verify_credentials(email: str, password: str) → int[source]

Given a user’s email and password, return the user’s in-game object id or else 0 to indicate verification failed.

textsmith.log

Configure structured logging.

Copyright (C) 2020 Nicholas H.Tollervey (ntoll@ntoll.org).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>

textsmith.log.host_info(logger, log_method, event_dict: dict) → dict[source]

Add useful information to each log entry about the system upon which the application is running.

textsmith.parser

Functions for parsing the user input. Calls into the game logic layer to affect changes and read data from the datastore.

Copyright (C) 2020 Nicholas H.Tollervey (ntoll@ntoll.org).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>

class textsmith.parser.Parser(logic: textsmith.logic.Logic)[source]

Gathers together methods to parse user input. Uses the dependency injection pattern.

eval(user_id: int, connection_id: str, message: str) → None[source]

Evaluate the user’s input message. If there’s an error, recover by sending the error message from the associated exception object.

handle_exception(user_id: int, connection_id: str, message_id: str, message: str, exception: Exception) → None[source]

Given an exception raised in the logic or parsing layer of the game, extract the useful message which explains what the problem is, and turn it into a message back to the referenced user.

parse(user_id: int, connection_id: str, message_id: str, message: str) → None[source]

Parse the incoming message from the referenced user.

There are four special characters which, if they start the message, act as shortcuts for common communication related activities:

  • " - the user says whatever follows in the message.
  • ! - make it appear like the user is shouting the message.
  • : - “emote” the message directly as “username ” + message.
  • @ - the user is saying something directly to another @user.

Next the parser expects the first word of the message to be a verb. If this verb is one of several built-in commands, the remainder of the message is passed as a single string into the relevant function for that verb (as defined in the verbs module). These verbs are translated by Babel, so the equivalent verbs in the user’s preferred locale (if supported by TextSmith) should work instead.

If the verb isn’t built into the game engine, then the parser breaks the raw input apart into sections that follow the following patterns:

VERB
VERB DIRECT-OBJECT
VERB DIRECT-OBJECT PREPOSITION INDIRECT-OBJECT

Examples of these patterns are:

look
take sword
give big sword to andrew
say "Hello there" to nicholas

NOTE: English articles (“a”, “the” etc) shouldn’t be used in commands.

Verbs that start sentences are assumed to be single words. Direct objects and indirect objects may be identified via multiple words.

Anything enclosed in double-quotes (“) is treated as a single entity if in the direct-object or indirect-object position. The parser will try to match objects against available aliases available in the current room’s context. If there are no matches or multiple matches then the parser will retain the string representation of the direct-object or indirect-object.

The following lists of reserved words are synonyms:

  • constants.USER_ALIASES - the user.
  • constants.ROOM_ALIASES - the current location.

These reserved words are actually translated by Babel, so the equivalent terms in the user’s preferred locale (if supported by TextSmith) should work instead.

At this point the parser has identified the verb string, and the direct and indirect objects. It looks for a matching verb on the four following objects (in order or precedence):

  1. The user giving the command.
  2. The room the user is in (including where the verb is an exit name).
  3. The direct object (if an object in the database).
  4. The indirect object (if an object in the database).

The game checks each object in turn and, if it finds an attribute that matches the verb it attempts to “execute” it.

Mostly, the attribute’s value will be returned. However, if the attribute’s value is a string and that string starts with the characters defined in constants.IS_SCRIPT, then it’ll attempt to evaluate the rest of the string as a script (see the script module for more detail of how this works).

If such “executable” attributes are found then the associated code will be run with the following objects in scope:

  • user - a reference to the user who issued the command.
  • room - a reference to the room in which the user is situated.
  • exits - objects that allow the user to move out of the current room.
  • users - objects representing other users currently in the current room.
  • things - all the other objects currently in the room.
  • this - a reference to the object which matched the verb (the user, room or other object in scope).
  • direct_object - either the matching object or raw string for the direct object. This could be None.
  • preposition - a string containing the preposition. This could be None.
  • indirect_object - either the matching object or raw string for the indirect object. This could be None.
  • raw_input - the raw (html escaped) string from the user that caused the script to be run.

The user, room, direct_object and indirect_object objects can all be passed to a special “emit” function along with a message to display to that object (if the object is a user, it’ll be sent just to them, if the object is a room, the message will be sent to all users in that room).

That’s it!

textsmith.pubsub

The pub/sub message passing methods and handlers needed for TextSmith.

Copyright (C) 2020 Nicholas H.Tollervey (ntoll@ntoll.org).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>

class textsmith.pubsub.PubSub(subscriber: asyncio_redis.protocol.Subscription)[source]

Contains methods needed to manage listening for messages broadcast on the pub/sub layer of the game.

get_message(user_id: int) → str[source]

Return the next message in the message queue for the referenced user. Otherwise, return an empty string (indicating no messages).

listen() → None[source]

Listen to the messages on subscribed channels. Each channel represents an object ID. If the object ID is a user connected to this application, then it’s put into the message queue for that user, to be sent via the websocket connection.

stop() → None[source]

Cleanly stop listening to the Redis PubSub.

subscribe(user_id: int, connection_id: str) → None[source]

Ensure there’s an entry for the referenced user’s message queue. Add the user ID to the list of channels this instance subscribes to via Redis. Log this event.

unsubscribe(user_id: int, connection_id: str) → None[source]

Remove the user ID from the list of channels to which this instance subscribes via Redis. Delete the message queue for the referenced user. Log this event. If there are undelivered messages, log these.

textsmith.verbs

Contains definitions of verbs built into the system that encompass the core behaviours possible within the system.

Copyright (C) 2020 Nicholas H.Tollervey (ntoll@ntoll.org).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>

exception textsmith.verbs.UnknownVerb[source]

An exception that indicates the passed in verb was not found.

class textsmith.verbs.Verbs(logic: textsmith.logic.Logic)[source]

Contains definitions of built-in verbs. These express the core fundamental capabilities of the world. Everything else is expressed in the scripting language.

The methods for verbs should NOT be called directly. Rather just call the Verb object with the verb written as appropriately for the user’s locale and the translation to the actual method to use will happen automatically.

_emote(user_id: int, connection_id: str, message_id: str, message: str) → None[source]

Emote the message to everyone in the current location.

_say(user_id: int, connection_id: str, message_id: str, message: str) → None[source]

Say the message to everyone in the current location.

_shout(user_id: int, connection_id: str, message_id: str, message: str) → None[source]

Shout (<strong></strong>) the message to everyone in the current location.

_tell(user_id: int, connection_id: str, message_id: str, message: str) → None[source]

Say the message to a specific person whilst being overheard by everyone else in the current location.