Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,27 @@ git -C /home/medo/dev/talebound/monorepo worktree add --no-track \
-b codex/example-feature \
/home/medo/dev/talebound/worktrees/example-feature origin/main
```

## Quest settings and character participation

Quest managers can set a quest's status and open recruitment from **Edit → Quest
settings**. On a character's detail page, **Search for a quest** lists quests that
accept applications in the same world and game system. Row/grid preferences are
remembered in the browser.

Character managers can apply with an optional message, update a pending message,
and withdraw or leave. Quest managers review applications on the quest detail
page and can approve, decline, or remove characters. Character detail pages show
all their quests and applications, including completed quests. Finishing or
abandoning a quest closes recruitment automatically.

Pending/declined applications and application messages are visible only to the
relevant character/quest managers (including site administrators). Other visitors
see accepted participants without their messages. Existing applications must be
withdrawn before changing a character's world/system; quest world/system changes
retain the original restriction to universal modules with no applications.

This recovers the frontend and backend `archive/*/feature/quest-settings-and-characters`
work on the current monorepo and dependency versions. Story turns and in-game chat
remain separate future features. The quest handler regression tests run as part of
`scripts/worktree-dev/talebound-worktree-dev test-backend`.
34 changes: 34 additions & 0 deletions backend/api/servicecore/quest_character_visibility.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package servicecore

import (
"context"
db "github.com/the-medo/talebound-backend/db/sqlc"
)

// VisibleQuestCharacters exposes accepted participants publicly. Pending/declined
// applications and their letters are visible only to their character or quest managers.
func (core *ServiceCore) VisibleQuestCharacters(ctx context.Context, rows []db.QuestCharacter, moduleType db.ModuleType, id int32) []db.QuestCharacter {
permissions := &ModulePermission{NeedsSuperAdmin: true}
_, _, managerErr := core.CheckModuleTypePermissions(ctx, moduleType, id, permissions)
_, authErr := core.AuthorizeUserCookie(ctx)
visible := make([]db.QuestCharacter, 0, len(rows))
for _, row := range rows {
canManage := managerErr == nil
if !canManage && authErr == nil {
otherType, otherID := db.ModuleTypeCharacter, row.CharacterID
if moduleType == db.ModuleTypeCharacter {
otherType, otherID = db.ModuleTypeQuest, row.QuestID
}
_, _, err := core.CheckModuleTypePermissions(ctx, otherType, otherID, permissions)
canManage = err == nil
}
if !canManage {
if row.Approved != 1 {
continue
}
row.MotivationalLetter = ""
}
visible = append(visible, row)
}
return visible
}
66 changes: 66 additions & 0 deletions backend/api/services/characters/rpc_get_character_quests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package characters

import (
"context"
"encoding/json"
"github.com/the-medo/talebound-backend/api/apihelpers"
"github.com/the-medo/talebound-backend/converters"
db "github.com/the-medo/talebound-backend/db/sqlc"
"github.com/the-medo/talebound-backend/e"
"github.com/the-medo/talebound-backend/pb"
"github.com/the-medo/talebound-backend/validator"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)

func (server *ServiceCharacters) GetCharacterQuests(ctx context.Context, req *pb.GetCharacterQuestsRequest) (*pb.GetQuestCharactersResponse, error) {
violations := validateGetCharacterQuests(req)

if violations != nil {
return nil, e.InvalidArgumentError(violations)
}

questCharacters, err := server.Store.GetQuestCharactersByCharacterID(ctx, req.GetCharacterId())
if err != nil {
return nil, err
}

questCharacters = server.VisibleQuestCharacters(ctx, questCharacters, db.ModuleTypeCharacter, req.GetCharacterId())

rsp := &pb.GetQuestCharactersResponse{
QuestCharacters: make([]*pb.QuestCharacter, len(questCharacters)),
}

questIds := make([]int32, len(questCharacters))

for i, qc := range questCharacters {
rsp.QuestCharacters[i] = converters.ConvertQuestCharacter(qc)
questIds[i] = qc.QuestID
}

fetchInterface := &apihelpers.FetchInterface{
QuestIds: questIds,
}

fetchIdsHeader, err := json.Marshal(fetchInterface)

md := metadata.Pairs(
"X-Fetch-Ids", string(fetchIdsHeader),
)
err = grpc.SendHeader(ctx, md)
if err != nil {
return nil, err
}

return rsp, nil
}

func validateGetCharacterQuests(req *pb.GetCharacterQuestsRequest) (violations []*errdetails.BadRequest_FieldViolation) {

if err := validator.ValidateUniversalId(req.GetCharacterId()); err != nil {
violations = append(violations, e.FieldViolation("character_id", err))
}

return violations
}
19 changes: 17 additions & 2 deletions backend/api/services/characters/rpc_update_character.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,23 @@ func (server *ServiceCharacters) UpdateCharacter(ctx context.Context, req *pb.Up
return nil, err
}

// TODO - make WorldID and SystemID updateable only when they are "Universal world" or "Universal system"
// otherwise, another related entities can be out of sync with what is possible
// A character's applications must remain compatible with their quests.
if req.WorldId != nil || req.SystemId != nil {
current, err := server.Store.GetCharacterByID(ctx, req.GetCharacterId())
if err != nil {
return nil, err
}
changed := (req.WorldId != nil && req.GetWorldId() != current.WorldID.Int32) || (req.SystemId != nil && req.GetSystemId() != current.SystemID.Int32)
if changed {
applications, err := server.Store.GetQuestCharactersByCharacterID(ctx, req.GetCharacterId())
if err != nil {
return nil, err
}
if len(applications) > 0 {
return nil, status.Error(codes.FailedPrecondition, "withdraw quest applications before changing the character's world or system")
}
}
}

arg := db.UpdateCharacterParams{
CharacterID: req.GetCharacterId(),
Expand Down
27 changes: 24 additions & 3 deletions backend/api/services/modules/rpc_update_module_admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ func (server *ServiceModules) UpdateModuleAdmin(ctx context.Context, req *pb.Upd
return nil, err
}

//TODO: add AllowedEntityTypes and AllowedMenu here and to SQL query
arg := db.UpdateModuleAdminParams{
ModuleID: req.GetModuleId(),
UserID: req.GetUserId(),
Expand All @@ -45,8 +44,8 @@ func (server *ServiceModules) UpdateModuleAdmin(ctx context.Context, req *pb.Upd
AllowedEntityTypesPresent: req.GetAllowedEntityTypes() != nil,
AllowedEntityTypes: make([]db.EntityType, 0),
AllowedMenu: sql.NullBool{
Bool: req.GetSuperAdmin(),
Valid: req.SuperAdmin != nil,
Bool: req.GetAllowedMenu(),
Valid: req.AllowedMenu != nil,
},
}

Expand All @@ -56,6 +55,28 @@ func (server *ServiceModules) UpdateModuleAdmin(ctx context.Context, req *pb.Upd
}
}

if req.GetApproved() == 1 {
_, err := server.Store.UpsertUserModule(ctx, db.UpsertUserModuleParams{
ModuleID: req.GetModuleId(),
UserID: req.GetUserId(),
Admin: sql.NullBool{
Bool: true,
Valid: true,
},
Favorite: sql.NullBool{
Bool: true,
Valid: true,
},
Following: sql.NullBool{
Bool: true,
Valid: true,
},
})
if err != nil {
return nil, err
}
}

moduleAdmin, err := server.Store.UpdateModuleAdmin(ctx, arg)
if err != nil {
return nil, err
Expand Down
Loading
Loading