Fixing contrib test issues

This commit is contained in:
Griatch 2021-12-18 23:56:38 +01:00
parent 04a95297b5
commit 7f2b8c81d7
30 changed files with 248 additions and 565 deletions

View file

@ -59,7 +59,7 @@ Exits: northeast and east
and
PROTOTYPE_MODULES += [evennia.contrib.xyzgrid.prototypes]
PROTOTYPE_MODULES += [evennia.contrib.grid.xyzgrid.prototypes]
This will add the new ability to enter `evennia xyzgrid <option>` on the
command line. It will also make the `xyz_room` and `xyz_exit` prototypes
@ -107,7 +107,7 @@ The `evennia xyzgrid` is a custom launch option added only for this contrib.
The xyzgrid-contrib comes with a full grid example. Let's add it:
$ evennia xyzgrid add evennia.contrib.xyzgrid.example
$ evennia xyzgrid add evennia.contrib.grid.xyzgrid.example
You can now list the maps on your grid:
@ -120,7 +120,7 @@ about each map with the `show` subcommand:
$ evennia xyzgrid show "the small cave"
If you want to peek at how the grid's code, open
[evennia/contrib/xyzgrid/example.py](evennia.contrib.xyzgrid.example).
[evennia/contrib/grid/xyzgrid/example.py](evennia.contrib.xyzgrid.example).
(We'll explain the details in later sections).
So far the grid is 'abstract' and has no actual in-game presence. Let's
@ -430,9 +430,9 @@ LEGEND = {
The legend is optional, and any symbol not explicitly given in your legend will
fall back to its value in the default legend [outlined below](#default-legend).
- [MapNode](evennia.contrib.xyzgrid.xymap_legend.MapNode)
- [MapNode](evennia.contrib.grid.xyzgrid.xymap_legend.MapNode)
is the base class for all nodes.
- [MapLink](evennia.contrib.xyzgrid.xymap_legend.MapLink)
- [MapLink](evennia.contrib.grid.xyzgrid.xymap_legend.MapLink)
is the base class for all links.
As the _Map String_ is parsed, each found symbol is looked up in the legend and
@ -445,7 +445,7 @@ with a full set of map elements that use these properties in various ways
(described in the next section).
Some useful properties of the
[MapNode](evennia.contrib.xyzgrid.xymap_legend.MapNode)
[MapNode](evennia.contrib.grid.xyzgrid.xymap_legend.MapNode)
class (see class doc for hook methods):
- `symbol` (str) - The character to parse from the map into this node. By default this
@ -473,7 +473,7 @@ class (see class doc for hook methods):
useful for various reasons, mostly map-transitions).
Some useful properties of the
[MapLink](evennia.contrib.xyzgrid.xymap_legend.MapLink)
[MapLink](evennia.contrib.grid.xyzgrid.xymap_legend.MapLink)
class (see class doc for hook methods):
- `symbol` (str) - The character to parse from the map into this node. This must
@ -530,7 +530,7 @@ Below is an example that changes the map's nodes to show up as red
(maybe for a lava map?):
```
from evennia.contrib.xyzgrid import xymap_legend
from evennia.contrib.grid.xyzgrid import xymap_legend
class RedMapNode(xymap_legend.MapNode):
display_symbol = "|r#|n"
@ -548,7 +548,7 @@ LEGEND = {
Below is the default map legend. The `symbol` is what should be put in the Map
string. It must always be a single character. The `display-symbol` is what is
actually visualized when displaying the map to players in-game. This could have
colors etc. All classes are found in `evennia.contrib.xyzgrid.xymap_legend` and
colors etc. All classes are found in `evennia.contrib.grid.xyzgrid.xymap_legend` and
their names are included to make it easy to know what to override.
```{eval-rst}
@ -801,7 +801,7 @@ different (unused) unique symbol in your map legend:
```python
# in your map definition module
from evennia.contrib.xyzgrid import xymap_legend
from evennia.contrib.grid.xyzgrid import xymap_legend
MAPSTR = r"""
@ -851,7 +851,7 @@ added, with different map-legend symbols:
```python
# in your map definition module (let's say this is mapB)
from evennia.contrib.xyzgrid import xymap_legend
from evennia.contrib.grid.xyzgrid import xymap_legend
MAPSTR = r"""
@ -924,10 +924,10 @@ across the map boundary.
[Prototypes](../Components/Prototypes.md) are dicts that describe how to _spawn_ a new instance
of an object. Each of the _nodes_ and _links_ above have a default prototype
that allows the `evennia xyzgrid spawn` command to convert them to
a [XYZRoom](evennia.contrib.xyzgrid.xyzroom.XYZRoom)
or an [XYZExit](evennia.contrib.xyzgrid.xyzroom.XYZRoom) respectively.
a [XYZRoom](evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom)
or an [XYZExit](evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom) respectively.
The default prototypes are found in `evennia.contrib.xyzgrid.prototypes` (added
The default prototypes are found in `evennia.contrib.grid.xyzgrid.prototypes` (added
during installation of this contrib), with `prototype_key`s `"xyz_room"` and
`"xyz_exit"` - use these as `prototype_parent` to add your own custom prototypes.
@ -1012,7 +1012,7 @@ picked up and applied to the existing objects.
#### Extending the base prototypes
The default prototypes are found in `evennia.contrib.xyzgrid.prototypes` and
The default prototypes are found in `evennia.contrib.grid.xyzgrid.prototypes` and
should be included as `prototype_parents` for prototypes on the map. Would it
not be nice to be able to change these and have the change apply to all of the
grid? You can, by adding the following to your `mygame/server/conf/settings.py`:
@ -1177,9 +1177,9 @@ To access the grid in-code, there are several ways:
grid = evennia.search_script("XYZGrid")[0]
(`search_script` always returns a list)
- You can get it with `evennia.contrib.xyzgrid.xyzgrid.get_xyzgrid`
- You can get it with `evennia.contrib.grid.xyzgrid.xyzgrid.get_xyzgrid`
from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid
from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid
grid = get_xyzgrid()
This will *always* return a grid, creating an empty grid if one didn't

View file

@ -1,5 +1,5 @@
```{eval-rst}
evennia.contrib
evennia.contrib
=======================
.. automodule:: evennia.contrib
@ -54,6 +54,6 @@ evennia.contrib
evennia.contrib.turnbattle
evennia.contrib.tutorial_examples
evennia.contrib.tutorial_world
evennia.contrib.xyzgrid
evennia.contrib.grid.xyzgrid
```
```

View file

@ -1,5 +1,5 @@
```{eval-rst}
evennia.contrib.xyzgrid.commands
evennia.contrib.grid.xyzgrid.commands
=======================================
.. automodule:: evennia.contrib.xyzgrid.commands
@ -7,4 +7,4 @@ evennia.contrib.xyzgrid.commands
:undoc-members:
:show-inheritance:
```
```

View file

@ -2162,7 +2162,7 @@ class TestCommsChannel(CommandTest):
)
from evennia.comms import comms # noqa
from evennia.commands.default import comms # noqa
class TestComms(CommandTest):
@ -2179,13 +2179,13 @@ class TestComms(CommandTest):
class TestBatchProcess(CommandTest):
@patch("evennia.contrib.tutorial_examples.red_button.repeat")
@patch("evennia.contrib.tutorial_examples.red_button.delay")
@patch("evennia.contrib.tutorials.red_button.red_button.repeat")
@patch("evennia.contrib.tutorials.red_button.red_button.delay")
def test_batch_commands(self, mock_delay, mock_repeat):
# cannot test batchcode here, it must run inside the server process
self.call(
batchprocess.CmdBatchCommands(),
"example_batch_cmds",
"batchprocessor.example_batch_cmds",
"Running Batch-command processor - Automatic mode for example_batch_cmds",
)
# we make sure to delete the button again here to stop the running reactor

View file

@ -1,5 +1,5 @@
"""
Extened Room - Griatch 2012, vincent-lg 2019
Contribs related to moving in and manipulating the game world and grid.
"""

View file

@ -21,7 +21,7 @@ class ForceUTCDatetime(datetime.datetime):
return datetime.datetime.utcfromtimestamp(timestamp)
@patch("evennia.contrib.extended_room.datetime.datetime", ForceUTCDatetime)
@patch("evennia.contrib.grid.extended_room.extended_room.datetime.datetime", ForceUTCDatetime)
# mock gametime to return April 9, 2064, at 21:06 (spring evening)
@patch("evennia.utils.gametime.gametime", new=Mock(return_value=2975000766))
class TestExtendedRoom(CommandTest):

View file

@ -6,12 +6,193 @@ Test map builder.
from evennia.commands.default.tests import CommandTest
from . import mapbuilder
# -*- coding: utf-8 -*-
# Add the necessary imports for your instructions here.
from evennia import create_object
from typeclasses import rooms, exits
from random import randint
import random
# A map with a temple (▲) amongst mountains (n,∩) in a forest (♣,♠) on an
# island surrounded by water (≈). By giving no instructions for the water
# characters we effectively skip it and create no rooms for those squares.
EXAMPLE1_MAP = '''
n
n
'''
def example1_build_forest(x, y, **kwargs):
'''A basic example of build instructions. Make sure to include **kwargs
in the arguments and return an instance of the room for exit generation.'''
# Create a room and provide a basic description.
room = create_object(rooms.Room, key="forest" + str(x) + str(y))
room.db.desc = "Basic forest room."
# Send a message to the account
kwargs["caller"].msg(room.key + " " + room.dbref)
# This is generally mandatory.
return room
def example1_build_mountains(x, y, **kwargs):
'''A room that is a little more advanced'''
# Create the room.
room = create_object(rooms.Room, key="mountains" + str(x) + str(y))
# Generate a description by randomly selecting an entry from a list.
room_desc = [
"Mountains as far as the eye can see",
"Your path is surrounded by sheer cliffs",
"Haven't you seen that rock before?",
]
room.db.desc = random.choice(room_desc)
# Create a random number of objects to populate the room.
for i in range(randint(0, 3)):
rock = create_object(key="Rock", location=room)
rock.db.desc = "An ordinary rock."
# Send a message to the account
kwargs["caller"].msg(room.key + " " + room.dbref)
# This is generally mandatory.
return room
def example1_build_temple(x, y, **kwargs):
'''A unique room that does not need to be as general'''
# Create the room.
room = create_object(rooms.Room, key="temple" + str(x) + str(y))
# Set the description.
room.db.desc = (
"In what, from the outside, appeared to be a grand and "
"ancient temple you've somehow found yourself in the the "
"Evennia Inn! It consists of one large room filled with "
"tables. The bardisk extends along the east wall, where "
"multiple barrels and bottles line the shelves. The "
"barkeep seems busy handing out ale and chatting with "
"the patrons, which are a rowdy and cheerful lot, "
"keeping the sound level only just below thunderous. "
"This is a rare spot of mirth on this dread moor."
)
# Send a message to the account
kwargs["caller"].msg(room.key + " " + room.dbref)
# This is generally mandatory.
return room
# Include your trigger characters and build functions in a legend dict.
EXAMPLE1_LEGEND = {
("", ""): example1_build_forest,
("", "n"): example1_build_mountains,
(""): example1_build_temple,
}
# Example two
# @mapbuilder/two evennia.contrib.mapbuilder.EXAMPLE2_MAP EXAMPLE2_LEGEND
# -*- coding: utf-8 -*-
# Add the necessary imports for your instructions here.
# from evennia import create_object
# from typeclasses import rooms, exits
# from evennia.utils import utils
# from random import randint
# import random
# This is the same layout as Example 1 but included are characters for exits.
# We can use these characters to determine which rooms should be connected.
EXAMPLE2_MAP = '''
--
| |
| | |
--
'''
def example2_build_forest(x, y, **kwargs):
'''A basic room'''
# If on anything other than the first iteration - Do nothing.
if kwargs["iteration"] > 0:
return None
room = create_object(rooms.Room, key="forest" + str(x) + str(y))
room.db.desc = "Basic forest room."
kwargs["caller"].msg(room.key + " " + room.dbref)
return room
def example2_build_verticle_exit(x, y, **kwargs):
'''Creates two exits to and from the two rooms north and south.'''
# If on the first iteration - Do nothing.
if kwargs["iteration"] == 0:
return
north_room = kwargs["room_dict"][(x, y - 1)]
south_room = kwargs["room_dict"][(x, y + 1)]
# create exits in the rooms
create_object(
exits.Exit, key="south", aliases=["s"], location=north_room, destination=south_room
)
create_object(
exits.Exit, key="north", aliases=["n"], location=south_room, destination=north_room
)
kwargs["caller"].msg("Connected: " + north_room.key + " & " + south_room.key)
def example2_build_horizontal_exit(x, y, **kwargs):
'''Creates two exits to and from the two rooms east and west.'''
# If on the first iteration - Do nothing.
if kwargs["iteration"] == 0:
return
west_room = kwargs["room_dict"][(x - 1, y)]
east_room = kwargs["room_dict"][(x + 1, y)]
create_object(exits.Exit, key="east", aliases=["e"], location=west_room, destination=east_room)
create_object(exits.Exit, key="west", aliases=["w"], location=east_room, destination=west_room)
kwargs["caller"].msg("Connected: " + west_room.key + " & " + east_room.key)
# Include your trigger characters and build functions in a legend dict.
EXAMPLE2_LEGEND = {
("", ""): example2_build_forest,
("|"): example2_build_verticle_exit,
("-"): example2_build_horizontal_exit,
}
class TestMapBuilder(CommandTest):
def test_cmdmapbuilder(self):
self.call(
mapbuilder.CmdMapBuilder(),
"evennia.contrib.mapbuilder.EXAMPLE1_MAP evennia.contrib.mapbuilder.EXAMPLE1_LEGEND",
"evennia.contrib.grid.mapbuilder.tests.EXAMPLE1_MAP "
"evennia.contrib.grid.mapbuilder.tests.EXAMPLE1_LEGEND",
"""Creating Map...|≈≈≈≈≈
n
@ -21,7 +202,8 @@ class TestMapBuilder(CommandTest):
)
self.call(
mapbuilder.CmdMapBuilder(),
"evennia.contrib.mapbuilder.EXAMPLE2_MAP evennia.contrib.mapbuilder.EXAMPLE2_LEGEND",
"evennia.contrib.grid.mapbuilder.tests.EXAMPLE2_MAP "
"evennia.contrib.grid.mapbuilder.tests.EXAMPLE2_LEGEND",
"""Creating Map...|≈ ≈ ≈ ≈ ≈
--

View file

@ -12,7 +12,7 @@ class TestSimpleDoor(CommandTest):
def test_cmdopen(self):
self.call(
simpledoor.CmdOpen(),
"newdoor;door:contrib.simpledoor.SimpleDoor,backdoor;door = Room2",
"newdoor;door:contrib.grid.simpledoor.SimpleDoor,backdoor;door = Room2",
"Created new Exit 'newdoor' from Room to Room2 (aliases: door).|Note: A door-type exit was "
"created - ignored eventual custom return-exit type.|Created new Exit 'newdoor' from Room2 to Room (aliases: door).",
)

View file

@ -1,5 +1,5 @@
"""
XYZGrid - Evennia 2021
XYZGrid - Griatch 2021
"""

View file

@ -4,12 +4,12 @@ the `evennia xyzgrid` launcher command.
First add the launcher extension in your mygame/server/conf/settings.py:
EXTRA_LAUNCHER_COMMANDS['xyzgrid'] = 'evennia.contrib.xyzgrid.launchcmd.xyzcommand'
EXTRA_LAUNCHER_COMMANDS['xyzgrid'] = 'evennia.contrib.grid.xyzgrid.launchcmd.xyzcommand'
Then
evennia xyzgrid init
evennia xyzgrid add evennia.contrib.xyzgrid.map_example
evennia xyzgrid add evennia.contrib.grid.xyzgrid.map_example
evennia xyzgrid build
@ -21,7 +21,7 @@ from evennia.contrib.grid.xyzgrid import xymap_legend
# default prototype parent. It's important that
# the typeclass inherits from the XYZRoom (or XYZExit)
# if adding the evennia.contrib.xyzgrid.prototypes to
# if adding the evennia.contrib.grid.xyzgrid.prototypes to
# settings.PROTOTYPE_MODULES, one could just set the
# prototype_parent to 'xyz_room' and 'xyz_exit' here
# instead.
@ -30,14 +30,14 @@ ROOM_PARENT = {
"key": "An empty room",
"prototype_key": "xyz_exit_prototype",
# "prototype_parent": "xyz_room",
"typeclass": "evennia.contrib.xyzgrid.xyzroom.XYZRoom",
"typeclass": "evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom",
"desc": "An empty room.",
}
EXIT_PARENT = {
"prototype_key": "xyz_exit_prototype",
# "prototype_parent": "xyz_exit",
"typeclass": "evennia.contrib.xyzgrid.xyzroom.XYZExit",
"typeclass": "evennia.contrib.grid.xyzgrid.xyzroom.XYZExit",
"desc": "A path to the next location.",
}

View file

@ -5,7 +5,7 @@ server (since this can be slow).
To use, add to the settings:
::
EXTRA_LAUNCHER_COMMANDS.update({'xyzgrid': 'evennia.contrib.xyzgrid.launchcmd.xyzcommand'})
EXTRA_LAUNCHER_COMMANDS.update({'xyzgrid': 'evennia.contrib.grid.xyzgrid.launchcmd.xyzcommand'})
You should now be able to do
::
@ -80,7 +80,7 @@ add <path.to.xymap.module> [<path> <path>,...]
{"map": mapstring, "zcoord": mapname/zcoord, "legend": dict, "prototypes": dict}
describing one single XYmap, or
- a XYMAP_DATA_LIST - a list of multiple dicts on the XYMAP_DATA form. This allows for
embedding multiple maps in the same module. See evennia/contrib/xyzgrid/example.py
embedding multiple maps in the same module. See evennia/contrib/grid/xyzgrid/example.py
for an example of how this looks.
Note that adding a map does *not* spawn it. If maps are linked to one another, you should
@ -89,7 +89,7 @@ add <path.to.xymap.module> [<path> <path>,...]
Examples:
evennia xyzgrid add evennia.contrib.xyzgrid.example
evennia xyzgrid add evennia.contrib.grid.xyzgrid.example
evennia xyzgrid add world.mymap1 world.mymap2 world.mymap3
"""

View file

@ -3,7 +3,7 @@ Default prototypes for building the XYZ-grid into actual game-rooms.
Add this to mygame/conf/settings/settings.py:
PROTOTYPE_MODULES += ['evennia.contrib.xyzgrid.prototypes']
PROTOTYPE_MODULES += ['evennia.contrib.grid.xyzgrid.prototypes']
The prototypes can then be used in mapping prototypes as
@ -28,7 +28,7 @@ except AttributeError:
room_prototype = {
'prototype_key': 'xyz_room',
'typeclass': 'evennia.contrib.xyzgrid.xyzroom.XYZRoom',
'typeclass': 'evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom',
'prototype_tags': ("xyzroom", ),
'key': "A room",
'desc': "An empty room."
@ -37,7 +37,7 @@ room_prototype.update(room_override)
exit_prototype = {
'prototype_key': 'xyz_exit',
'typeclass': 'evennia.contrib.xyzgrid.xyzroom.XYZExit',
'typeclass': 'evennia.contrib.grid.xyzgrid.xyzroom.XYZExit',
'prototype_tags': ("xyzexit", ),
'desc': "An exit."
}

View file

@ -854,8 +854,6 @@ class TestMap8(_MapTest):
target=target, target_path_style="",
character='@',
max_size=max_size)
self.assertEqual(expected, mapstr.replace("||", "|"))
def test_spawn(self):
"""
Spawn the map into actual objects.
@ -1018,6 +1016,8 @@ class TestMap11(_MapTest):
target=target, target_path_style="",
character='@',
max_size=max_size)
print(f"\n\n{coord}-{target}\n{expected}\n\n{mapstr}")
self.assertEqual(expected, mapstr)
def test_spawn(self):
@ -1252,6 +1252,7 @@ class TestXYZGridTransition(EvenniaTest):
self.assertEqual(east_exit.db_destination, room2)
self.assertEqual(west_exit.db_destination, room1)
class TestBuildExampleGrid(EvenniaTest):
"""
Test building the map-example (this takes about 30s)
@ -1274,7 +1275,7 @@ class TestBuildExampleGrid(EvenniaTest):
Build the map example.
"""
mapdatas = self.grid.maps_from_module("evennia.contrib.xyzgrid.example")
mapdatas = self.grid.maps_from_module("evennia.contrib.grid.xyzgrid.example")
self.assertEqual(len(mapdatas), 2)
self.grid.add_maps(*mapdatas)

View file

@ -210,7 +210,7 @@ class XYMap:
if not _LOADED_PROTOTYPES:
# inject default prototypes, but don't override prototype-keys loaded from
# settings, if they exist (that means the user wants to replace the defaults)
protlib.load_module_prototypes("evennia.contrib.xyzgrid.prototypes", override=False)
protlib.load_module_prototypes("evennia.contrib.grid.xyzgrid.prototypes", override=False)
_LOADED_PROTOTYPES = True
self.Z = Z
@ -636,7 +636,7 @@ class XYMap:
"""
global _XYZROOMCLASS
if not _XYZROOMCLASS:
from evennia.contrib.xyzgrid.xyzroom import XYZRoom as _XYZROOMCLASS
from evennia.contrib.grid.xyzgrid.xyzroom import XYZRoom as _XYZROOMCLASS
x, y = xy
wildcard = '*'
spawned = []

View file

@ -315,7 +315,7 @@ class MapNode:
# create a new entity with proper coordinates etc
tclass = self.prototype['typeclass']
tclass = (f' ({tclass})'
if tclass != 'evennia.contrib.xyzgrid.xyzroom.XYZRoom'
if tclass != 'evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom'
else '')
self.log(f" spawning room at xyz={xyz}{tclass}")
nodeobj, err = NodeTypeclass.create(
@ -413,7 +413,7 @@ class MapNode:
prot = maplinks[key.lower()][3].prototype
tclass = prot['typeclass']
tclass = (f' ({tclass})'
if tclass != 'evennia.contrib.xyzgrid.xyzroom.XYZExit'
if tclass != 'evennia.contrib.grid.xyzgrid.xyzroom.XYZExit'
else '')
self.log(f" spawning/updating exit xyz={xyz}, direction={key}{tclass}")

View file

@ -278,7 +278,7 @@ class XYZRoom(DefaultRoom):
def xyzgrid(self):
global GET_XYZGRID
if not GET_XYZGRID:
from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID
from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID
return GET_XYZGRID()
@property
@ -493,7 +493,7 @@ class XYZExit(DefaultExit):
def xyzgrid(self):
global GET_XYZGRID
if not GET_XYZGRID:
from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID
from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID
return GET_XYZGRID()
@property

View file

@ -4,7 +4,7 @@
# It allows batch processing of normal Evennia commands.
# Test it by loading it with @batchcommand:
#
# @batchcommand[/interactive] examples.batch_example
# batchcommand[/interactive] examples.batch_example
#
# A # as the first symbol on a line begins a comment and
# marks the end of a previous command definition (important!).
@ -17,12 +17,12 @@
# This creates a red button
@create button:tutorial_examples.red_button.RedButton
create button:red_button.RedButton
# This comment ends input for @create
# Next command:
@set button/desc =
set button/desc =
This is a large red button. Now and then
it flashes in an evil, yet strangely tantalizing way.
@ -48,7 +48,7 @@ know you want to!
# Now let's place the button where it belongs (let's say limbo #2 is
# the evil lair in our example).
@teleport #2
teleport #2
#... and drop it (remember, this comment ends input to @teleport, so don't
#forget it!) The very last command in the file need not be ended with #.

View file

@ -45,7 +45,7 @@
# all other #CODE blocks when they are executed.
from evennia import create_object, search_object
from evennia.contrib.tutorial_examples import red_button
from evennia.contrib.tutorials import red_button
from evennia import DefaultObject
limbo = search_object("Limbo")[0]

View file

@ -32,14 +32,14 @@ Files included in this module:
Deployment is completed by configuring a few settings in server.conf. This line
is required:
SERVER_SESSION_CLASS = 'evennia.contrib.security.auditing.server.AuditedServerSession'
SERVER_SESSION_CLASS = 'evennia.contrib.utils.auditing.server.AuditedServerSession'
This tells Evennia to use this ServerSession instead of its own. Below are the
other possible options along with the default value that will be used if unset.
# Where to send logs? Define the path to a module containing your callback
# function. It should take a single dict argument as input
AUDIT_CALLBACK = 'evennia.contrib.security.auditing.outputs.to_file'
AUDIT_CALLBACK = 'evennia.contrib.utils.auditing.outputs.to_file'
# Log user input? Be ethical about this; it will log all private and
# public communications between players and/or admins (default: False).

View file

@ -16,7 +16,7 @@ from evennia.server.serversession import ServerSession
# Attributes governing auditing of commands and where to send log objects
AUDIT_CALLBACK = getattr(
ev_settings, "AUDIT_CALLBACK", "evennia.contrib.security.auditing.outputs.to_file"
ev_settings, "AUDIT_CALLBACK", "evennia.contrib.utils.auditing.outputs.to_file"
)
AUDIT_IN = getattr(ev_settings, "AUDIT_IN", False)
AUDIT_OUT = getattr(ev_settings, "AUDIT_OUT", False)

View file

@ -8,13 +8,13 @@ from evennia.utils.test_resources import EvenniaTest
import re
# Configure session auditing settings - TODO: This is bad practice that leaks over to other tests
settings.AUDIT_CALLBACK = "evennia.security.contrib.auditing.outputs.to_syslog"
settings.AUDIT_CALLBACK = "evennia.contrib.utils.auditing.outputs.to_syslog"
settings.AUDIT_IN = True
settings.AUDIT_OUT = True
settings.AUDIT_ALLOW_SPARSE = True
# Configure settings to use custom session - TODO: This is bad practice, changing global settings
settings.SERVER_SESSION_CLASS = "evennia.contrib.security.auditing.server.AuditedServerSession"
settings.SERVER_SESSION_CLASS = "evennia.contrib.utils.auditing.server.AuditedServerSession"
class AuditingTest(EvenniaTest):

View file

@ -399,7 +399,7 @@ class ServerSessionHandler(SessionHandler):
self.server.at_post_portal_sync(mode)
# announce the reconnection
if _BROADCAST_SERVER_RESTART_MESSAGES:
self.announce_all(_(" ... Server restarted."))
self.announce_all(_(" ... Server restarted."))
def portal_disconnect(self, session):
"""

View file

@ -508,7 +508,7 @@ TYPECLASS_PATHS = [
"typeclasses",
"evennia",
"evennia.contrib",
"evennia.contrib.tutorial_examples",
"evennia.contrib.tutorials",
]
# Typeclass for account objects (linked to a character) (fallback)
@ -602,7 +602,7 @@ VALIDATOR_FUNC_MODULES = ["evennia.utils.validatorfuncs"]
BASE_BATCHPROCESS_PATHS = [
"world",
"evennia.contrib",
"evennia.contrib.tutorial_examples",
"evennia.contrib.tutorials",
]
######################################################################

View file

@ -1,5 +0,0 @@
# Security
This directory contains security-related contribs
- Auditing (Johnny 2018) - Allow for optional security logging of user input/output.

View file

@ -1,72 +0,0 @@
# Input/Output Auditing
Contrib - Johnny 2017
This is a tap that optionally intercepts all data sent to/from clients and the
server and passes it to a callback of your choosing.
It is intended for quality assurance, post-incident investigations and debugging
but obviously can be abused. All data is recorded in cleartext. Please
be ethical, and if you are unwilling to properly deal with the implications of
recording user passwords or private communications, please do not enable
this module.
Some checks have been implemented to protect the privacy of users.
Files included in this module:
outputs.py - Example callback methods. This module ships with examples of
callbacks that send data as JSON to a file in your game/server/logs
dir or to your native Linux syslog daemon. You can of course write
your own to do other things like post them to Kafka topics.
server.py - Extends the Evennia ServerSession object to pipe data to the
callback upon receipt.
tests.py - Unit tests that check to make sure commands with sensitive
arguments are having their PII scrubbed.
Installation/Configuration:
Deployment is completed by configuring a few settings in server.conf. This line
is required:
SERVER_SESSION_CLASS = 'evennia.contrib.security.auditing.server.AuditedServerSession'
This tells Evennia to use this ServerSession instead of its own. Below are the
other possible options along with the default value that will be used if unset.
# Where to send logs? Define the path to a module containing your callback
# function. It should take a single dict argument as input
AUDIT_CALLBACK = 'evennia.contrib.security.auditing.outputs.to_file'
# Log user input? Be ethical about this; it will log all private and
# public communications between players and/or admins (default: False).
AUDIT_IN = False
# Log server output? This will result in logging of ALL system
# messages and ALL broadcasts to connected players, so on a busy game any
# broadcast to all users will yield a single event for every connected user!
AUDIT_OUT = False
# The default output is a dict. Do you want to allow key:value pairs with
# null/blank values? If you're just writing to disk, disabling this saves
# some disk space, but whether you *want* sparse values or not is more of a
# consideration if you're shipping logs to a NoSQL/schemaless database.
# (default: False)
AUDIT_ALLOW_SPARSE = False
# If you write custom commands that handle sensitive data like passwords,
# you must write a regular expression to remove that before writing to log.
# AUDIT_MASKS is a list of dictionaries that define the names of commands
# and the regexes needed to scrub them.
# The system already has defaults to filter out sensitive login/creation
# commands in the default command set. Your list of AUDIT_MASKS will be appended
# to those defaults.
#
# In the regex, the sensitive data itself must be captured in a named group with a
# label of 'secret' (see the Python docs on the `re` module for more info). For
# example: `{'authentication': r"^@auth\s+(?P<secret>[\w]+)"}`
AUDIT_MASKS = []

View file

@ -1,60 +0,0 @@
"""
Auditable Server Sessions - Example Outputs
Example methods demonstrating output destinations for logs generated by
audited server sessions.
This is designed to be a single source of events for developers to customize
and add any additional enhancements before events are written out-- i.e. if you
want to keep a running list of what IPs a user logs in from on account/character
objects, or if you want to perform geoip or ASN lookups on IPs before committing,
or tag certain events with the results of a reputational lookup, this should be
the easiest place to do it. Write a method and invoke it via
`settings.AUDIT_CALLBACK` to have log data objects passed to it.
Evennia contribution - Johnny 2017
"""
from evennia.utils.logger import log_file
import json
import syslog
def to_file(data):
"""
Writes dictionaries of data generated by an AuditedServerSession to files
in JSON format, bucketed by date.
Uses Evennia's native logger and writes to the default
log directory (~/yourgame/server/logs/ or settings.LOG_DIR)
Args:
data (dict): Parsed session transmission data.
"""
# Bucket logs by day and remove objects before serialization
bucket = data.pop("objects")["time"].strftime("%Y-%m-%d")
# Write it
log_file(json.dumps(data), filename="audit_%s.log" % bucket)
def to_syslog(data):
"""
Writes dictionaries of data generated by an AuditedServerSession to syslog.
Takes advantage of your system's native logger and writes to wherever
you have it configured, which is independent of Evennia.
Linux systems tend to write to /var/log/syslog.
If you're running rsyslog, you can configure it to dump and/or forward logs
to disk and/or an external data warehouse (recommended-- if your server is
compromised or taken down, losing your logs along with it is no help!).
Args:
data (dict): Parsed session transmission data.
"""
# Remove objects before serialization
data.pop("objects")
# Write it out
syslog.syslog(json.dumps(data))

View file

@ -1,249 +0,0 @@
"""
Auditable Server Sessions:
Extension of the stock ServerSession that yields objects representing
user inputs and system outputs.
Evennia contribution - Johnny 2017
"""
import os
import re
import socket
from django.utils import timezone
from django.conf import settings as ev_settings
from evennia.utils import utils, logger, mod_import, get_evennia_version
from evennia.server.serversession import ServerSession
# Attributes governing auditing of commands and where to send log objects
AUDIT_CALLBACK = getattr(
ev_settings, "AUDIT_CALLBACK", "evennia.contrib.security.auditing.outputs.to_file"
)
AUDIT_IN = getattr(ev_settings, "AUDIT_IN", False)
AUDIT_OUT = getattr(ev_settings, "AUDIT_OUT", False)
AUDIT_ALLOW_SPARSE = getattr(ev_settings, "AUDIT_ALLOW_SPARSE", False)
AUDIT_MASKS = [
{"connect": r"^[@\s]*[connect]{5,8}\s+(\".+?\"|[^\s]+)\s+(?P<secret>.+)"},
{"connect": r"^[@\s]*[connect]{5,8}\s+(?P<secret>[\w]+)"},
{"create": r"^[^@]?[create]{5,6}\s+(\w+|\".+?\")\s+(?P<secret>[\w]+)"},
{"create": r"^[^@]?[create]{5,6}\s+(?P<secret>[\w]+)"},
{"userpassword": r"^[@\s]*[userpassword]{11,14}\s+(\w+|\".+?\")\s+=*\s*(?P<secret>[\w]+)"},
{"userpassword": r"^.*new password set to '(?P<secret>[^']+)'\."},
{"userpassword": r"^.* has changed your password to '(?P<secret>[^']+)'\."},
{"password": r"^[@\s]*[password]{6,9}\s+(?P<secret>.*)"},
] + getattr(ev_settings, "AUDIT_MASKS", [])
if AUDIT_CALLBACK:
try:
AUDIT_CALLBACK = getattr(
mod_import(".".join(AUDIT_CALLBACK.split(".")[:-1])), AUDIT_CALLBACK.split(".")[-1]
)
logger.log_sec("Auditing module online.")
logger.log_sec(
"Audit record User input: {}, output: {}.\n"
"Audit sparse recording: {}, Log callback: {}".format(
AUDIT_IN, AUDIT_OUT, AUDIT_ALLOW_SPARSE, AUDIT_CALLBACK
)
)
except Exception as e:
logger.log_err("Failed to activate Auditing module. %s" % e)
class AuditedServerSession(ServerSession):
"""
This particular implementation parses all server inputs and/or outputs and
passes a dict containing the parsed metadata to a callback method of your
creation. This is useful for recording player activity where necessary for
security auditing, usage analysis or post-incident forensic discovery.
*** WARNING ***
All strings are recorded and stored in plaintext. This includes those strings
which might contain sensitive data (create, connect, @password). These commands
have their arguments masked by default, but you must mask or mask any
custom commands of your own that handle sensitive information.
See README.md for installation/configuration instructions.
"""
def audit(self, **kwargs):
"""
Extracts messages and system data from a Session object upon message
send or receive.
Keyword Args:
src (str): Source of data; 'client' or 'server'. Indicates direction.
text (str or list): Client sends messages to server in the form of
lists. Server sends messages to client as string.
Returns:
log (dict): Dictionary object containing parsed system and user data
related to this message.
"""
# Get time at start of processing
time_obj = timezone.now()
time_str = str(time_obj)
session = self
src = kwargs.pop("src", "?")
bytecount = 0
# Do not log empty lines
if not kwargs:
return {}
# Get current session's IP address
client_ip = session.address
# Capture Account name and dbref together
account = session.get_account()
account_token = ""
if account:
account_token = "%s%s" % (account.key, account.dbref)
# Capture Character name and dbref together
char = session.get_puppet()
char_token = ""
if char:
char_token = "%s%s" % (char.key, char.dbref)
# Capture Room name and dbref together
room = None
room_token = ""
if char:
room = char.location
room_token = "%s%s" % (room.key, room.dbref)
# Try to compile an input/output string
def drill(obj, bucket):
if isinstance(obj, dict):
return bucket
elif utils.is_iter(obj):
for sub_obj in obj:
bucket.extend(drill(sub_obj, []))
else:
bucket.append(obj)
return bucket
text = kwargs.pop("text", "")
if utils.is_iter(text):
text = "|".join(drill(text, []))
# Mask any PII in message, where possible
bytecount = len(text.encode("utf-8"))
text = self.mask(text)
# Compile the IP, Account, Character, Room, and the message.
log = {
"time": time_str,
"hostname": socket.getfqdn(),
"application": "%s" % ev_settings.SERVERNAME,
"version": get_evennia_version(),
"pid": os.getpid(),
"direction": "SND" if src == "server" else "RCV",
"protocol": self.protocol_key,
"ip": client_ip,
"session": "session#%s" % self.sessid,
"account": account_token,
"character": char_token,
"room": room_token,
"text": text.strip(),
"bytes": bytecount,
"data": kwargs,
"objects": {
"time": time_obj,
"session": self,
"account": account,
"character": char,
"room": room,
},
}
# Remove any keys with blank values
if AUDIT_ALLOW_SPARSE is False:
log["data"] = {k: v for k, v in log["data"].items() if v}
log["objects"] = {k: v for k, v in log["objects"].items() if v}
log = {k: v for k, v in log.items() if v}
return log
def mask(self, msg):
"""
Masks potentially sensitive user information within messages before
writing to log. Recording cleartext password attempts is bad policy.
Args:
msg (str): Raw text string sent from client <-> server
Returns:
msg (str): Text string with sensitive information masked out.
"""
# Check to see if the command is embedded within server output
_msg = msg
is_embedded = False
match = re.match(".*Command.*'(.+)'.*is not available.*", msg, flags=re.IGNORECASE)
if match:
msg = match.group(1).replace("\\", "")
submsg = msg
is_embedded = True
for mask in AUDIT_MASKS:
for command, regex in mask.items():
try:
match = re.match(regex, msg, flags=re.IGNORECASE)
except Exception as e:
logger.log_err(regex)
logger.log_err(e)
continue
if match:
term = match.group("secret")
masked = re.sub(term, "*" * len(term.zfill(8)), msg)
if is_embedded:
msg = re.sub(
submsg, "%s <Masked: %s>" % (masked, command), _msg, flags=re.IGNORECASE
)
else:
msg = masked
return msg
return _msg
def data_out(self, **kwargs):
"""
Generic hook for sending data out through the protocol.
Keyword Args:
kwargs (any): Other data to the protocol.
"""
if AUDIT_CALLBACK and AUDIT_OUT:
try:
log = self.audit(src="server", **kwargs)
if log:
AUDIT_CALLBACK(log)
except Exception as e:
logger.log_err(e)
super(AuditedServerSession, self).data_out(**kwargs)
def data_in(self, **kwargs):
"""
Hook for protocols to send incoming data to the engine.
Keyword Args:
kwargs (any): Other data from the protocol.
"""
if AUDIT_CALLBACK and AUDIT_IN:
try:
log = self.audit(src="client", **kwargs)
if log:
AUDIT_CALLBACK(log)
except Exception as e:
logger.log_err(e)
super(AuditedServerSession, self).data_in(**kwargs)

View file

@ -1,114 +0,0 @@
"""
Module containing the test cases for the Audit system.
"""
from anything import Anything
from django.test import override_settings
from django.conf import settings
from evennia.utils.test_resources import EvenniaTest
import re
# Configure session auditing settings - TODO: This is bad practice that leaks over to other tests
settings.AUDIT_CALLBACK = "evennia.security.contrib.auditing.outputs.to_syslog"
settings.AUDIT_IN = True
settings.AUDIT_OUT = True
settings.AUDIT_ALLOW_SPARSE = True
# Configure settings to use custom session - TODO: This is bad practice, changing global settings
settings.SERVER_SESSION_CLASS = "evennia.contrib.security.auditing.server.AuditedServerSession"
class AuditingTest(EvenniaTest):
def test_mask(self):
"""
Make sure the 'mask' function is properly masking potentially sensitive
information from strings.
"""
safe_cmds = (
"/say hello to my little friend",
"@ccreate channel = for channeling",
"@create/drop some stuff",
"@create rock",
"@create a pretty shirt : evennia.contrib.clothing.Clothing",
"@charcreate johnnyefhiwuhefwhef",
'Command "@logout" is not available. Maybe you meant "@color" or "@cboot"?',
'/me says, "what is the password?"',
"say the password is plugh",
# Unfortunately given the syntax, there is no way to discern the
# latter of these as sensitive
"@create pretty sunset" "@create johnny password123",
'{"text": "Command \'do stuff\' is not available. Type "help" for help."}',
)
for cmd in safe_cmds:
self.assertEqual(self.session.mask(cmd), cmd)
unsafe_cmds = (
(
"something - new password set to 'asdfghjk'.",
"something - new password set to '********'.",
),
(
"someone has changed your password to 'something'.",
"someone has changed your password to '*********'.",
),
("connect johnny password123", "connect johnny ***********"),
("concnct johnny password123", "concnct johnny ***********"),
("concnct johnnypassword123", "concnct *****************"),
('connect "johnny five" "password 123"', 'connect "johnny five" **************'),
('connect johnny "password 123"', "connect johnny **************"),
("create johnny password123", "create johnny ***********"),
("@password password1234 = password2345", "@password ***************************"),
("@password password1234 password2345", "@password *************************"),
("@passwd password1234 = password2345", "@passwd ***************************"),
("@userpassword johnny = password234", "@userpassword johnny = ***********"),
("craete johnnypassword123", "craete *****************"),
(
"Command 'conncect teddy teddy' is not available. Maybe you meant \"@encode\"?",
"Command 'conncect ******** ********' is not available. Maybe you meant \"@encode\"?",
),
(
"{'text': u'Command \\'conncect jsis dfiidf\\' is not available. Type \"help\" for help.'}",
"{'text': u'Command \\'conncect jsis ********\\' is not available. Type \"help\" for help.'}",
),
)
for index, (unsafe, safe) in enumerate(unsafe_cmds):
self.assertEqual(re.sub(" <Masked: .+>", "", self.session.mask(unsafe)).strip(), safe)
# Make sure scrubbing is not being abused to evade monitoring
secrets = [
"say password password password; ive got a secret that i cant explain",
"whisper johnny = password\n let's lynch the landlord",
"say connect johnny password1234|the secret life of arabia",
"@password eval(\"__import__('os').system('clear')\", {'__builtins__':{}})",
]
for secret in secrets:
self.assertEqual(self.session.mask(secret), secret)
def test_audit(self):
"""
Make sure the 'audit' function is returning a dictionary based on values
parsed from the Session object.
"""
log = self.session.audit(src="client", text=[["hello"]])
obj = {
k: v for k, v in log.items() if k in ("direction", "protocol", "application", "text")
}
self.assertEqual(
obj,
{
"direction": "RCV",
"protocol": "telnet",
"application": Anything, # this will change if running tests from the game dir
"text": "hello",
},
)
# Make sure OOB data is being recorded
log = self.session.audit(
src="client", text="connect johnny password123", prompt="hp=20|st=10|ma=15", pane=2
)
self.assertEqual(log["text"], "connect johnny ***********")
self.assertEqual(log["data"]["prompt"], "hp=20|st=10|ma=15")
self.assertEqual(log["data"]["pane"], 2)