延長 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 端點的子端點,只能存取字元。

# in mygame/web/api/views.py

# we'll need these from django's rest framework to make our view work
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status

# this implements all the basic Evennia Object endpoint logic, so we're inheriting from it
from evennia.web.api.views import ObjectDBViewSet

# and we need this to filter our character view
from evennia.objects.objects import DefaultCharacter

# our own custom view
class CharacterViewSet(ObjectDBViewSet):
    """
    A customized Character view that adds an inventory detail
    """
    queryset = DefaultCharacter.objects.all_family()

設定 url

現在我們有了自己的檢視集,我們可以建立自己的 urls 模組並更改 characters 端點路徑以指向我們的端點路徑。

API 路由比網站或 webclient 路由更複雜,因此您需要將整個模組從 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 # <--- NEW

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") # <--- MODIFIED
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 auto-doc (based on 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 - 新增到您的 Web 根 url 模組。否則它將繼續指向預設的 API 路由器,我們將永遠不會看到我們的變更。

在編輯器中開啟 mygame/web/urls.py 並為「api/」新增路徑,指向 web.api.urls。最終檔案應如下所示:

# mygame/web/urls.py

from django.urls import path, include

# default evennia patterns
from evennia.web.urls import urlpatterns as evennia_default_urlpatterns

# add patterns
urlpatterns = [
    # website
    path("", include("web.website.urls")),
    # webclient
    path("webclient/", include("web.webclient.urls")),
    # web admin
    path("admin/", include("web.admin.urls")),
        
    # the new API path
    path("api/", include("web.api.urls")),
]

# 'urlpatterns' must be named such for django to find it.
urlpatterns = urlpatterns + evennia_default_urlpatterns

重新啟動您的 evennia 遊戲 - 從指令列 evennia reboot 完全重新啟動遊戲 AND portal - 並再次嘗試獲取 /api/characters/。如果它的工作原理與以前完全一樣,那麼您就可以繼續下一步了!

新增細節

返回您的角色檢視類別 - 是時候開始新增我們的庫存了。

REST API 中常見的「頁面」稱為端點,是您通常造訪的內容。 e.g。 /api/characters/ 是「字元」端點,/api/characters/:id 是單一字元的端點。

然而,端點也可以有一個或多個「詳細」檢視,其功能類似於子點。我們將新增 inventory 作為我們的角色端點的詳細資訊,它看起來像 /api/characters/:id/inventory

使用 django REST 框架,新增新細節就像在檢視集合類別中新增裝飾方法一樣簡單 - @action 裝飾器。由於檢查您的庫存只是資料檢索,因此我們只想允許 GET 方法,並且我們將此操作新增為 API 詳細資訊,因此我們的裝飾器將如下所示:

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

在某些情況下,您可能需要的詳細資訊或端點不僅僅是資料檢索:例如,拍賣行清單中的「購買」或「出售」。在這些情況下,您可以使用 putpost 來代替。若要進一步瞭解 @action 和 ViewSets 的用途,請造訪 django REST 框架檔案

當新增函式作為詳細資訊操作時,函式的名稱將與詳細資訊相同。由於我們想要 inventory 操作,因此我們將定義一個 inventory 函式。

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

Customized views for the REST API
"""
# we'll need these from django's rest framework to make our view work
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status

# this implements all the basic Evennia Object endpoint logic, so we're inheriting from it
from evennia.web.api.views import ObjectDBViewSet

# and we need this to filter our character view
from evennia.objects.objects import DefaultCharacter

# our own custom view
class CharacterViewSet(ObjectDBViewSet):
    """
    A customized Character view that adds an inventory detail
    """
    queryset = DefaultCharacter.objects.all_family()

    # !! NEW
    @action(detail=True, methods=["get"])
    def inventory(self, request, pk=None):
        return Response("your inventory", 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 並首先新增您需要的匯入。

# the base serializing library for the framework
from rest_framework import serializers

# the handy classes Evennia already prepared for us
from evennia.web.api.serializers import TypeclassSerializerMixin, SimpleObjectDBSerializer

# and the DefaultObject typeclass, for the necessary db model information
from evennia.objects.objects import DefaultObject

接下來,我們將定義我們自己的序列化器類別。由於它用於檢索庫存資料,因此我們將對其進行適當的命名。

class InventorySerializer(TypeclassSerializerMixin, serializers.ModelSerializer):
    """
    Serializing an inventory
    """
    
    # these define the groups of items
    worn = serializers.SerializerMethodField()
    carried = serializers.SerializerMethodField()
    
    class Meta:
        model = DefaultObject
        fields = [
            "id", # required field
            # add these to match the properties you defined
            "worn",
            "carried",
        ]
        read_only_fields = ["id"]

Meta 類別定義了最終序列化字串中將使用哪些欄位。 id 欄位來自基礎 ModelSerializer,但您會注意到其他兩個 - worncarried - 定義為 SerializerMethodField 的屬性。這告訴框架在序列化時尋找 get_X 形式的匹配方法名稱。

這就是為什麼我們的下一步是新增這些方法!我們定義了屬性 worncarried,因此我們將新增的方法是 get_wornget_carried。它們將是靜態方法 - 也就是說,它們不包含 self - 因為它們不需要引用序列化器類別本身。

    # these methods filter the character's contents based on the `worn` attribute
    def get_worn(character):
        """
        Serializes only worn objects in the target's inventory.
        """
        worn = [obj for obj in character.contents if obj.db.worn]
        return SimpleObjectDBSerializer(worn, many=True).data
    
    def get_carried(character):
        """
        Serializes only non-worn objects in the target's inventory.
        """
        carried = [obj for obj in character.contents if not obj.db.worn]
        return SimpleObjectDBSerializer(carried, many=True).data

對於本指南,我們假設物件是否被磨損儲存在 worn 資料庫 attribute 中,並基於該 attribute 進行過濾。這可以很容易地以不同的方式完成,以匹配您自己的遊戲機制:根據 tag 進行過濾,在您的角色上呼叫返回正確列表的自訂方法等。

如果您想新增更多詳細資訊 - 透過輸入將攜帶的物品分組,或分割盔甲與武器,您只需要新增或變更屬性、欄位和方法。

請記住:worn = serializers.SerializerMethodField() 是 API 知道如何使用 get_wornMeta.fields 是實際將其放入最終 JSON 的欄位清單。

您的最終檔案應如下所示:

# mygame/web/api/serializers.py

# the base serializing library for the framework
from rest_framework import serializers

# the handy classes Evennia already prepared for us
from evennia.web.api.serializers import TypeclassSerializerMixin, SimpleObjectDBSerializer

# and the DefaultObject typeclass, for the necessary db model information
from evennia.objects.objects import DefaultObject

class InventorySerializer(TypeclassSerializerMixin, serializers.ModelSerializer):
    """
    Serializing an inventory
    """
    
    # these define the groups of items
    worn = serializers.SerializerMethodField()
    carried = serializers.SerializerMethodField()
    
    class Meta:
        model = DefaultObject
        fields = [
            "id", # required field
            # add these to match the properties you defined
            "worn",
            "carried",
        ]
        read_only_fields = ["id"]

    # these methods filter the character's contents based on the `worn` attribute
    def get_worn(character):
        """
        Serializes only worn objects in the target's inventory.
        """
        worn = [obj for obj in character.contents if obj.db.worn]
        return SimpleObjectDBSerializer(worn, many=True).data
    
    def get_carried(character):
        """
        Serializes only non-worn objects in the target's inventory.
        """
        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

Customized views for the REST API
"""
# we'll need these from django's rest framework to make our view work
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status

# this implements all the basic Evennia Object endpoint logic, so we're inheriting from it
from evennia.web.api.views import ObjectDBViewSet

# and we need this to filter our character view
from evennia.objects.objects import DefaultCharacter

from .serializers import InventorySerializer # <--- NEW

# our own custom view
class CharacterViewSet(ObjectDBViewSet):
    """
    A customized Character view that adds an inventory detail
    """
    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 ) # <--- MODIFIED

這將使用我們新的序列化器來獲取角色的庫存。除了……不完全是。

繼續嘗試: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):
        """
        Checks object-level permissions after has_permission
        """
        # our new permission check
        if view.action == "inventory":
            return self.check_locks(obj, request.user, self.view_locks)

        # if it's not an inventory action, run through all the default checks
        return super().has_object_permission(request, view, obj)

這就是整個許可權類別!對於我們的最後一步,我們需要透過匯入它並設定 permission_classes 屬性來在角色檢視中使用它。

完成後,最終的 views.py 應如下:

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

Customized views for the REST API
"""
# we'll need these from django's rest framework to make our view work
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status

# this implements all the basic Evennia Object endpoint logic, so we're inheriting from it
from evennia.web.api.views import ObjectDBViewSet

# and we need this to filter our character view
from evennia.objects.objects import DefaultCharacter

from .serializers import InventorySerializer
from .permissions import CharacterPermission # <--- NEW

# our own custom view
class CharacterViewSet(ObjectDBViewSet):
    """
    A customized Character view that adds an inventory detail
    """
    permission_classes = [CharacterPermission] # <--- NEW
    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 現有字元 desc PUT 一個新 desc。 (提示:檢視 evennia 的 REST 許可權模組如何運作,以及預設 evennia REST API 檢視中的 set_attribute 方法。)