扩展 REST API

默认情况下,Evennia 的 REST API 为标准实体提供端点。其中一个端点是 /api/characters/,返回角色的信息。在本教程中,我们将通过向 /characters 端点添加一个 inventory 操作来扩展它,显示角色所 穿戴携带 的所有物品。

创建自己的视图集

您需要做的第一件事是定义您自己的 views.py 模块。

创建一个空文件:mygame/web/api/views.py

默认的 REST API 端点由 evennia/web/api/views.py 中的类控制——您可以复制整个文件并使用它,但我们将专注于进行最小的更改。

首先,我们将重新实现处理来自 characters/ 端点请求的默认 CharacterViewSet。这是一个只能访问角色的 objects 端点的子类。

# 在 mygame/web/api/views.py 中

# 我们需要从 Django 的 REST 框架中获取这些内容来使我们的视图工作
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status

# 这实现了所有基本的 Evennia 对象端点逻辑,因此我们从中继承
from evennia.web.api.views import ObjectDBViewSet

# 我们还需要这个来过滤我们的角色视图
from evennia.objects.objects import DefaultCharacter

# 我们自己的自定义视图
class CharacterViewSet(ObjectDBViewSet):
    """
    自定义的角色视图,添加了库存细节
    """
    queryset = DefaultCharacter.objects.all_family()

设置 URL

现在我们有了自己的视图集,可以创建自己的 URL 模块,并将 characters 端点路径更改为指向我们自己的视图。

API 路由比网站或网页客户端路由更复杂,因此您需要将整个模块从 Evennia 复制到您的游戏中,而不是进行补丁性的更改。将文件从 evennia/web/api/urls.py 复制到您的文件夹 mygame/web/api/urls.py 并在编辑器中打开它。

导入您的新视图模块,然后找到并更新 characters 路径以使用您自己的视图集。

# mygame/web/api/urls.py

from django.urls import path
from django.views.generic import TemplateView
from rest_framework.schemas import get_schema_view

from evennia.web.api.root import APIRootRouter
from evennia.web.api import views

from . import views as my_views # <--- 新增

app_name = "api"

router = APIRootRouter()
router.trailing_slash = "/?"
router.register(r"accounts", views.AccountDBViewSet, basename="account")
router.register(r"objects", views.ObjectDBViewSet, basename="object")
router.register(r"characters", my_views.CharacterViewSet, basename="character") # <--- 修改
router.register(r"exits", views.ExitViewSet, basename="exit")
router.register(r"rooms", views.RoomViewSet, basename="room")
router.register(r"scripts", views.ScriptDBViewSet, basename="script")
router.register(r"helpentries", views.HelpViewSet, basename="helpentry")

urlpatterns = router.urls

urlpatterns += [
    # openapi schema
    path(
        "openapi",
        get_schema_view(title="Evennia API", description="Evennia OpenAPI Schema", version="1.0"),
        name="openapi",
    ),
    # redoc 自动文档(基于 openapi schema)
    path(
        "redoc/",
        TemplateView.as_view(
            template_name="rest_framework/redoc.html", extra_context={"schema_url": "api:openapi"}
        ),
        name="redoc",
    ),
]

我们几乎已经将它指向新的视图。最后一步是将你的 API URL - web.api.urls - 添加到你的网站根 URL 模块。否则它将继续指向默认的 API 路由器,我们永远不会看到我们的更改。

在编辑器中打开 mygame/web/urls.py,并为 “api/” 添加一个新路径,指向 web.api.urls。最终文件应如下所示:

# mygame/web/urls.py

from django.urls import path, include

# 默认的 Evennia 模式
from evennia.web.urls import urlpatterns as evennia_default_urlpatterns

# 添加模式
urlpatterns = [
    # 网站
    path("", include("web.website.urls")),
    # 网页客户端
    path("webclient/", include("web.webclient.urls")),
    # 网站管理
    path("admin/", include("web.admin.urls")),
        
    # 新的 API 路径
    path("api/", include("web.api.urls")),
]

# 'urlpatterns' 必须命名,以便 Django 能找到它。
urlpatterns = urlpatterns + evennia_default_urlpatterns

重启您的 Evennia 游戏 - 从命令行使用 evennia reboot 完全重启游戏和门户 - 然后再次尝试访问 /api/characters/。如果它的工作方式与之前完全相同,那么您就可以继续进行下一步了!

添加新细节

回到您的角色视图类——现在是时候开始添加库存了。

REST API 中的常规“页面”称为 端点,这是您通常访问的内容。例如,/api/characters/ 是“角色”端点,而 /api/characters/:id 是单个角色的端点。

然而,端点也可以有一个或多个 详细 视图,其功能类似于子端点。我们将添加 inventory 作为角色端点的详细信息,形式为 /api/characters/:id/inventory

使用 Django REST 框架,添加新细节的方法很简单,就是在视图集类中添加一个装饰的方法 - @action 装饰器。由于检查库存只是数据检索,我们只想允许 GET 方法,并且我们将此操作作为 API 细节,因此我们的装饰器看起来如下:

@action(detail=True, methods=["get"])

有些情况下,您可能希望某个细节或端点不只是数据检索:例如,拍卖行列表中的 buysell。在这些情况下,您将使用 putpost。有关使用 @action 和视图集可以做的事情的更多阅读,请访问 Django REST 框架文档

当将函数作为详细操作添加时,函数的名称将与操作的名称相同。由于我们希望有一个 inventory 操作,因此我们将定义一个 inventory 函数。

"""
mygame/web/api/views.py

用于 REST API 的自定义视图
"""
# 我们需要从 Django 的 REST 框架中获取这些内容来使我们的视图工作
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status

# 这实现了所有基本的 Evennia 对象端点逻辑,因此我们从中继承
from evennia.web.api.views import ObjectDBViewSet

# 和我们需要这个来过滤我们的角色视图
from evennia.objects.objects import DefaultCharacter

# 我们自己的自定义视图
class CharacterViewSet(ObjectDBViewSet):
    """
    自定义的角色视图,添加了库存细节
    """
    queryset = DefaultCharacter.objects.all_family()

    # !! 新增
    @action(detail=True, methods=["get"])
    def inventory(self, request, pk=None):
        return Response("您的库存", status=status.HTTP_200_OK)

获取角色的 ID - 它与您的 dbref 一样,但没有 # - 然后再次执行 evennia reboot。现在您应该能够调用您的新角色操作:/api/characters/1/inventory(假设您正在查看角色 #1),它将返回字符串 “您的库存”。

创建序列化器

然而,仅仅返回一个简单的字符串并没有多大用处。我们想要的是角色的实际库存——为此,我们需要设置自己的 序列化器

一般来说,序列化器 会将一组数据转换为特殊格式的字符串,以便在数据流中发送 - 通常是 JSON。Django REST 序列化器是特别的类和函数,可以将 Python 对象转换为 API 准备就绪的格式。因此,就像视图集一样,Django 和 Evennia 已经为我们完成了很多繁重的工作。

我们将继承 Evennia 的现有序列化器并扩展它以满足我们的需求,而不是自己编写序列化器。为此,创建一个新文件 mygame/web/api/serializers.py 并开始添加您所需的导入。

# 框架的基本序列化库
from rest_framework import serializers

# Evennia 为我们准备的便利类
from evennia.web.api.serializers import TypeclassSerializerMixin, SimpleObjectDBSerializer

# 和必要的数据库模型信息的 DefaultObject 类型类
from evennia.objects.objects import DefaultObject

接下来,我们将定义自己的序列化器类。由于它用于检索库存数据,因此我们将其命名为适当。

class InventorySerializer(TypeclassSerializerMixin, serializers.ModelSerializer):
    """
    序列化库存
    """
    
    # 这些定义了物品的组
    worn = serializers.SerializerMethodField()
    carried = serializers.SerializerMethodField()
    
    class Meta:
        model = DefaultObject
        fields = [
            "id", # 必需字段
            # 添加这些以匹配您定义的属性
            "worn",
            "carried",
        ]
        read_only_fields = ["id"]

Meta 类定义了将在最终序列化字符串中使用的字段。id 字段来自基础 ModelSerializer,但您会注意到另外两个字段 - worncarried - 被定义为 SerializerMethodField 的属性。这告诉框架在序列化时查找形如 get_X 的匹配方法名称。

这就是我们下一步要添加这些方法的原因!我们定义了属性 worncarried,因此我们将添加 get_wornget_carried 方法。它们将是静态方法——即不包含 self——因为它们不需要引用序列化器类本身。

    # 这些方法根据 `worn` 属性过滤角色的内容
    def get_worn(character):
        """
        仅序列化目标库存中穿戴的物品。
        """
        worn = [obj for obj in character.contents if obj.db.worn]
        return SimpleObjectDBSerializer(worn, many=True).data
    
    def get_carried(character):
        """
        仅序列化目标库存中未穿戴的物品。
        """
        carried = [obj for obj in character.contents if not obj.db.worn]
        return SimpleObjectDBSerializer(carried, many=True).data

在本指南中,我们假设物品是否被穿戴存储在 worn 数据库属性中,并基于该属性进行过滤。根据您的游戏机制,这可以轻松地以不同的方式进行匹配:根据标签进行过滤,调用角色的自定义方法以返回正确的列表等。

如果您想添加更多详细信息 - 按类型对携带的物品进行分组,或将盔甲与武器区分开,您只需添加或更改属性、字段和方法。

请记住:worn = serializers.SerializerMethodField() 是 API 知道使用 get_worn 的方法,而 Meta.fields 则是最终将“输入” JSON 的字段列表。

您的最终文件应如下所示:

# mygame/web/api/serializers.py

# 框架的基本序列化库
from rest_framework import serializers

# Evennia 为我们准备的便利类
from evennia.web.api.serializers import TypeclassSerializerMixin, SimpleObjectDBSerializer

# 和必要的数据库模型信息的 DefaultObject 类型类
from evennia.objects.objects import DefaultObject

class InventorySerializer(TypeclassSerializerMixin, serializers.ModelSerializer):
    """
    序列化库存
    """
    
    # 这些定义了物品的组
    worn = serializers.SerializerMethodField()
    carried = serializers.SerializerMethodField()
    
    class Meta:
        model = DefaultObject
        fields = [
            "id", # 必需字段
            # 添加这些以匹配您定义的属性
            "worn",
            "carried",
        ]
        read_only_fields = ["id"]

    # 这些方法根据 `worn` 属性过滤角色的内容
    def get_worn(character):
        """
        仅序列化目标库存中穿戴的物品。
        """
        worn = [obj for obj in character.contents if obj.db.worn]
        return SimpleObjectDBSerializer(worn, many=True).data
    
    def get_carried(character):
        """
        仅序列化目标库存中未穿戴的物品。
        """
        carried = [obj for obj in character.contents if not obj.db.worn]
        return SimpleObjectDBSerializer(carried, many=True).data

使用序列化器

现在让我们回到视图文件 mygame/web/api/views.py。将我们的新序列化器与其他导入一起添加:

from .serializers import InventorySerializer

然后,更新我们的 inventory 详细信息以使用我们的序列化器。

    @action(detail=True, methods=["get"])
    def inventory(self, request, pk=None):
        obj = self.get_object()
        return Response( InventorySerializer(obj).data, status=status.HTTP_200_OK )

现在您的视图文件应如下所示:

"""
mygame/web/api/views.py

用于 REST API 的自定义视图
"""
# 我们需要从 Django 的 REST 框架中获取这些内容来使我们的视图工作
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status

# 这实现了所有基本的 Evennia 对象端点逻辑,因此我们从中继承
from evennia.web.api.views import ObjectDBViewSet

# 和我们需要这个来过滤我们的角色视图
from evennia.objects.objects import DefaultCharacter

from .serializers import InventorySerializer # <--- 新增

# 我们自己的自定义视图
class CharacterViewSet(ObjectDBViewSet):
    """
    自定义的角色视图,添加了库存细节
    """
    queryset = DefaultCharacter.objects.all_family()

    @action(detail=True, methods=["get"])
    def inventory(self, request, pk=None):
        return Response( InventorySerializer(obj).data, status=status.HTTP_200_OK ) # <--- 修改

这将使用我们的新序列化器来获取角色的库存。不过……并不完全。

继续尝试:evennia reboot,然后像之前一样访问 /api/characters/1/inventory。此时,您应该得到一个错误,提示您没有权限。别担心——这意味着它成功引用了新的序列化器。我们只需没有赋予它访问对象的权限。

自定义 API 权限

Evennia 带有其自己的自定义 API 权限类,将 API 权限与游戏内权限级别和锁定系统连接起来。由于我们现在试图访问对象的数据,因此我们需要通过 has_object_permission 检查以及一般权限检查——而默认的权限类在对象权限检查中硬编码了动作。

由于我们向角色端点添加了一个新操作 - inventory,我们还需要在角色端点上使用我们自己的自定义权限。再创建一个模块文件:mygame/web/api/permissions.py

与之前的类一样,我们将从原始类继承并扩展其功能,以利用 Evennia 已经为我们做的所有工作。

# mygame/web/api/permissions.py

from evennia.web.api.permissions import EvenniaPermission

class CharacterPermission(EvenniaPermission):
    
    def has_object_permission(self, request, view, obj):
        """
        在 has_permission 之后检查对象级别的权限
        """
        # 我们的新权限检查
        if view.action == "inventory":
            return self.check_locks(obj, request.user, self.view_locks)

        # 如果不是一个库存操作,则通过所有默认检查
        return super().has_object_permission(request, view, obj)

这就是整个权限类!在最后一步中,我们需要通过导入它并设置 permission_classes 属性来在角色视图中使用它。

完成后,您的最终 views.py 应如下所示:

"""
mygame/web/api/views.py

用于 REST API 的自定义视图
"""
# 我们需要从 Django 的 REST 框架中获取这些内容来使我们的视图工作
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status

# 这实现了所有基本的 Evennia 对象端点逻辑,因此我们从中继承
from evennia.web.api.views import ObjectDBViewSet

# 和我们需要这个来过滤我们的角色视图
from evennia.objects.objects import DefaultCharacter

from .serializers import InventorySerializer
from .permissions import CharacterPermission # <--- 新增

# 我们自己的自定义视图
class CharacterViewSet(ObjectDBViewSet):
    """
    自定义的角色视图,添加了库存细节
    """
    permission_classes = [CharacterPermission] # <--- 新增
    queryset = DefaultCharacter.objects.all_family()

    @action(detail=True, methods=["get"])
    def inventory(self, request, pk=None):
        obj = self.get_object()
        return Response( InventorySerializer(obj).data, status=status.HTTP_200_OK )

最后一次 evennia reboot - 现在您应该能够访问 /api/characters/1/inventory,查看您角色的所有物品,整齐划分为“穿戴”和“携带”。

下一步

就是这样!您已经学习了如何自定义自己的 Evennia REST 端点,添加新的端点细节,以及从游戏对象序列化数据以供 REST API 使用。通过这些工具,您可以获取任何想要的游戏内数据,使其可用 - 甚至可修改 - 通过 API。

如果您想要一个挑战,尝试将您所学的内容应用于实现一个新的 desc 细节,以便您可以 GET 获取现有角色描述 PUT 新描述。(提示:查看 Evennia 的 REST 权限模块的工作原理,以及默认 Evennia REST API 视图中的 set_attribute 方法。)