Introduction to entitlement revocation
Extend is in Open Beta for AGS Premium Clients! This means Extend available for you to try on your development environment. You can submit your feedback through this link.
Extend is currently available for AGS Starter Closed Beta partners only.
Overview
AccelByte Gaming Services (AGS) has the capability to provide custom logic for handling the revocation of player assets with Extend. In this guide, we will present the contract of the customization with example code on how to implement a revoke function that will send the necessary information to identify the user, the object, and the quantity to be revoked.
There's only one function on the contract, seen in the snippet below which is the unary function Revoke
:
service Revocation {
/**
Revoke
Currently, only Third-Party DLC Refund and Refund Order will trigger this grpc revocation.
*/
rpc Revoke(RevokeRequest) returns (RevokeResponse);
}
Revoke
Revoke
allows for the handling of revocation requests triggered by specific events within a gaming or virtual economy system. It is used for implementing the logic to manage entitlements and ensures the proper handling of refunds or revocation orders.
In this example, we'll generate some custom responses for the three types of entries that are currently supported; ITEM
, ENTITLEMENT
, and CURRENCY
.
Code Example:
- Go
- Python
- C#
- Java
The Revoke
method will run the correct function based on the revocation entry type
passed in.
func (s *RevocationServiceServer) Revoke(ctx context.Context, req *pb.RevokeRequest) (*pb.RevokeResponse, error) {
revocationEntryType := revocation.RevokeEntryType(req.GetRevokeEntryType())
revocationObj, err := revocation.GetRevocation(revocationEntryType)
if err != nil {
return &pb.RevokeResponse{
Status: revocation.StatusFail,
Reason: err.Error(),
}, nil
}
revocationResp, err := revocationObj.Revoke(req.GetNamespace(), req.GetUserId(), req.GetQuantity(), req)
if err != nil {
return &pb.RevokeResponse{
Status: revocation.StatusFail,
Reason: err.Error(),
}, nil
}
return revocationResp, nil
}
You will be able to modify the logic for those functions under revocation.go
.
The Revoke
method will run the correct function based on the revokeEntryType
passed in.
async def Revoke(self, request: RevokeRequest, context):
self.log_payload(f'{self.Revoke.__name__} request: %s', request)
response : RevokeResponse
try:
namespace = request.namespace
userId = request.userId
quantity = request.quantity
revoke_entry_type = RevokeEntryType[request.revokeEntryType.upper()]
revocation = Revocations().get_revocation(revoke_entry_type)
response = revocation.revoke(namespace, userId, quantity, request)
except Exception as e:
response = RevokeResponse(
reason = f"Revocation method {str(e)} not supported",
status = RevocationStatus.FAIL.name,
)
self.log_payload(f'{self.Revoke.__name__} response: %s', response)
return response
public override Task<RevokeResponse> Revoke(RevokeRequest request, ServerCallContext context)
{
var response = new RevokeResponse();
response.Status = "SUCCESS";
string revokeType = request.RevokeEntryType.Trim().ToUpper();
if (revokeType == "ITEM")
{
...
}
else if (revokeType == "CURRENCY")
{
...
}
else if (revokeType == "ENTITLEMENT")
{
...
}
else
{
response.Status = "FAIL";
response.Reason = $"Revocation type [{revokeType}] is not supported.";
}
return Task.FromResult(response);
}
@Override
public void revoke(RevokeRequest request, StreamObserver<RevokeResponse> responseObserver) {
RevokeResponse response;
try {
String namespace = request.getNamespace();
String userId = request.getUserId();
int quantity = request.getQuantity();
RevokeEntryType revokeEntryType = RevokeEntryType.valueOf(request.getRevokeEntryType().toUpperCase());
Revocation revocation = Revocations.getRevocation(revokeEntryType);
response = revocation.revoke(namespace, userId, quantity, request);
} catch (Throwable ex) {
response = RevokeResponse.newBuilder()
.setStatus(RevocationStatus.FAIL.name())
.setReason(ex.getMessage())
.build();
}
responseObserver.onNext(response);
responseObserver.onCompleted();
}
When the ITEM
entry type is passed in:
- Go
- Python
- C#
- Java
func (r *ItemRevocation) Revoke(namespace string, userId string, quantity int32, request *pb.RevokeRequest) (*pb.RevokeResponse, error) {
item := request.GetItem()
customRevocation := map[string]string{}
customRevocation["namespace"] = namespace
customRevocation["userId"] = userId
customRevocation["quantity"] = fmt.Sprintf("%d", quantity)
customRevocation["itemId"] = item.GetItemId()
customRevocation["sku"] = item.GetItemSku()
customRevocation["itemType"] = item.GetItemType()
customRevocation["useCount"] = fmt.Sprintf("%d", item.GetUseCount())
customRevocation["entitlementType"] = item.GetEntitlementType()
return &pb.RevokeResponse{
Status: StatusSuccess,
CustomRevocation: customRevocation,
}, nil
}
class ItemRevocation(Revocation):
def __init__(self) -> None:
self.custom_revocation = dict()
super().__init__()
def revoke(self, namespace, userId, quantity, request):
item = request.item
self.custom_revocation["namespace"] = namespace
self.custom_revocation["userId"] = userId
self.custom_revocation["quantity"] = str(quantity)
self.custom_revocation["sku"] = item.itemSku
self.custom_revocation["itemType"] = item.itemType
self.custom_revocation["useCount"] = str(item.useCount)
self.custom_revocation["entitlementType"] = item.entitlementType
return RevokeResponse(
customRevocation = self.custom_revocation,
status = RevocationStatus.SUCCESS.name,
)
response.CustomRevocation.Add(new Dictionary<string, string>()
{
{ "namespace", request.Namespace },
{ "userId", request.UserId },
{ "quantity", request.Quantity.ToString() },
{ "itemId", request.Item.ItemId },
{ "sku", request.Item.ItemSku },
{ "itemType", request.Item.ItemType },
{ "useCount", request.Item.UseCount.ToString() },
{ "entitlementType", request.Item.EntitlementType }
});
public class ItemRevocation implements Revocation {
@Override
public RevokeResponse revoke(String namespace, String userId, int quantity, RevokeRequest request) {
Map<String, String> customRevocation = new HashMap<>();
// execute your logic, this is for demo only
RevokeItemObject item = request.getItem();
customRevocation.put("namespace", namespace);
customRevocation.put("userId", userId);
customRevocation.put("quantity", String.valueOf(quantity));
customRevocation.put("itemId", item.getItemId());
customRevocation.put("sku", item.getItemSku());
customRevocation.put("itemType", item.getItemType());
customRevocation.put("useCount", String.valueOf(item.getUseCount()));
customRevocation.put("entitlementType", item.getEntitlementType());
return RevokeResponse.newBuilder()
.putAllCustomRevocation(customRevocation)
.setStatus(RevocationStatus.SUCCESS.name()).build();
}
}
When CURRENCY
entry is passed in:
- Go
- Python
- C#
- Java
func (r *CurrencyRevocation) Revoke(namespace string, userId string, quantity int32, request *pb.RevokeRequest) (*pb.RevokeResponse, error) {
currency := request.GetCurrency()
customRevocation := map[string]string{}
customRevocation["namespace"] = namespace
customRevocation["userId"] = userId
customRevocation["quantity"] = fmt.Sprintf("%d", quantity)
customRevocation["currencyNamespace"] = currency.GetNamespace()
customRevocation["currencyCode"] = currency.GetCurrencyCode()
customRevocation["balanceOrigin"] = currency.GetBalanceOrigin()
return &pb.RevokeResponse{
Status: StatusSuccess,
CustomRevocation: customRevocation,
}, nil
}
class CurrencyRevocation(Revocation):
def __init__(self) -> None:
self.custom_revocation = dict()
super().__init__()
def revoke(self, namespace, userId, quantity, request):
currency = request.currency
self.custom_revocation["namespace"] = namespace
self.custom_revocation["userId"] = userId
self.custom_revocation["quantity"] = str(quantity)
self.custom_revocation["currencyNamespace"] = currency.namespace
self.custom_revocation["currencyCode"] = currency.currencyCode
self.custom_revocation["balanceOrigin"] = currency.balanceOrigin
return RevokeResponse(
customRevocation = self.custom_revocation,
status = RevocationStatus.SUCCESS.name,
)
response.CustomRevocation.Add(new Dictionary<string, string>()
{
{ "namespace", request.Namespace },
{ "userId", request.UserId },
{ "quantity", request.Quantity.ToString() },
{ "currencyNamespace", request.Currency.Namespace },
{ "currencyCode", request.Currency.CurrencyCode },
{ "balanceOrigin", request.Currency.BalanceOrigin }
});
public class CurrencyRevocation implements Revocation {
@Override
public RevokeResponse revoke(String namespace, String userId, int quantity, RevokeRequest request) {
Map<String, String> customRevocation = new HashMap<>();
// execute your logic, this is for demo only
RevokeCurrencyObject currency = request.getCurrency();
customRevocation.put("namespace", namespace);
customRevocation.put("userId", userId);
customRevocation.put("quantity", String.valueOf(quantity));
customRevocation.put("currencyNamespace", currency.getNamespace());
customRevocation.put("currencyCode", currency.getCurrencyCode());
customRevocation.put("balanceOrigin", currency.getBalanceOrigin());
return RevokeResponse.newBuilder()
.putAllCustomRevocation(customRevocation)
.setStatus(RevocationStatus.SUCCESS.name()).build();
}
}
When the ENTITLEMENT
entry is passed in:
- Go
- Python
- C#
- Java
func (r *EntitlementRevocation) Revoke(namespace string, userId string, quantity int32, request *pb.RevokeRequest) (*pb.RevokeResponse, error) {
entitlement := request.GetEntitlement()
customRevocation := map[string]string{}
customRevocation["namespace"] = namespace
customRevocation["userId"] = userId
customRevocation["quantity"] = fmt.Sprintf("%d", quantity)
customRevocation["entitlementId"] = entitlement.GetEntitlementId()
customRevocation["itemId"] = entitlement.GetItemId()
customRevocation["sku"] = entitlement.GetSku()
return &pb.RevokeResponse{
Status: StatusSuccess,
CustomRevocation: customRevocation,
}, nil
}
class EntitlementRevocation(Revocation):
def __init__(self) -> None:
self.custom_revocation = dict()
super().__init__()
def revoke(self, namespace, userId, quantity, request):
entitlement = request.entitlement
self.custom_revocation["namespace"] = namespace
self.custom_revocation["userId"] = userId
self.custom_revocation["quantity"] = str(quantity)
self.custom_revocation["entitlementId"] = entitlement.entitlementId
self.custom_revocation["itemId"] = entitlement.itemId
self.custom_revocation["sku"] = entitlement.sku
return RevokeResponse(
customRevocation = self.custom_revocation,
status = RevocationStatus.SUCCESS.name,
)
response.CustomRevocation.Add(new Dictionary<string, string>()
{
{ "namespace", request.Namespace },
{ "userId", request.UserId },
{ "quantity", request.Quantity.ToString() },
{ "entitlementId", request.Entitlement.EntitlementId },
{ "itemId", request.Entitlement.ItemId },
{ "sku", request.Entitlement.Sku },
});
public class EntitlementRevocation implements Revocation {
@Override
public RevokeResponse revoke(String namespace, String userId, int quantity, RevokeRequest request) {
Map<String, String> customRevocation = new HashMap<>();
// execute your logic, this is for demo only
RevokeEntitlementObject entitlement = request.getEntitlement();
customRevocation.put("namespace", namespace);
customRevocation.put("userId", userId);
customRevocation.put("quantity", String.valueOf(quantity));
customRevocation.put("entitlementId", entitlement.getEntitlementId());
customRevocation.put("itemId", entitlement.getItemId());
customRevocation.put("sku", entitlement.getSku());
return RevokeResponse.newBuilder()
.putAllCustomRevocation(customRevocation)
.setStatus(RevocationStatus.SUCCESS.name()).build();
}
}