FILE / ScuroNeko/Laniakea
policy.go
Исходный файл и его история в репозитории.
Instrument command, payload, update, scene, runner, polling, and policy flows Add observer regression coverage and mark observability backlog item done
225 lines
6.2 KiB
Go
225 lines
6.2 KiB
Go
package laniakea
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
|
)
|
|
|
|
// Policy defines a reusable authorization rule for the current update context.
|
|
type Policy[T AppData] func(ctx *MsgContext, data T) error
|
|
|
|
// RequirePolicy adapts a Policy into a blocking middleware.
|
|
func RequirePolicy[T AppData](name string, p Policy[T]) Middleware[T] {
|
|
return NewMiddleware(name, func(ctx *MsgContext, data T) bool {
|
|
if err := p(ctx, data); err != nil {
|
|
ctx.emitPolicyChecked(PolicyCheckedEvent{
|
|
Name: name,
|
|
FromID: ctx.FromID,
|
|
ChatID: ctx.ChatID,
|
|
Passed: false,
|
|
Err: err,
|
|
Internal: IsInternalError(err),
|
|
})
|
|
ctx.error(err)
|
|
return false
|
|
}
|
|
ctx.emitPolicyChecked(PolicyCheckedEvent{
|
|
Name: name,
|
|
FromID: ctx.FromID,
|
|
ChatID: ctx.ChatID,
|
|
Passed: true,
|
|
})
|
|
return true
|
|
})
|
|
}
|
|
|
|
// AllPolicies composes policies that all must succeed.
|
|
func AllPolicies[T AppData](policies ...Policy[T]) Policy[T] {
|
|
return func(ctx *MsgContext, data T) error {
|
|
for _, p := range policies {
|
|
if err := p(ctx, data); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// AnyPolicy composes policies where at least one must succeed.
|
|
func AnyPolicy[T AppData](policies ...Policy[T]) Policy[T] {
|
|
return func(ctx *MsgContext, data T) error {
|
|
var firstDeny error
|
|
var internalErr error
|
|
for _, p := range policies {
|
|
err := p(ctx, data)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if IsInternalError(err) {
|
|
if internalErr == nil {
|
|
internalErr = err
|
|
}
|
|
continue
|
|
}
|
|
if firstDeny == nil {
|
|
firstDeny = err
|
|
}
|
|
}
|
|
if internalErr != nil {
|
|
return internalErr
|
|
}
|
|
if firstDeny != nil {
|
|
return firstDeny
|
|
}
|
|
return AsUserError(errors.New("no policy matched"))
|
|
}
|
|
}
|
|
|
|
// NotPolicy inverts a policy deny result while preserving internal failures.
|
|
func NotPolicy[T AppData](policy Policy[T]) Policy[T] {
|
|
return func(ctx *MsgContext, data T) error {
|
|
var err error
|
|
if err = policy(ctx, data); err == nil {
|
|
return AsUserError(errors.New("the action is not allowed due to policy violation"))
|
|
}
|
|
if IsInternalError(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RequirePrivateChat allows execution only in private chats.
|
|
func RequirePrivateChat[T AppData]() Policy[T] {
|
|
return func(ctx *MsgContext, data T) error {
|
|
if ctx.Msg == nil || ctx.Msg.Chat == nil {
|
|
return AsInternalError(errors.New("private-chat policy requires message chat context"))
|
|
}
|
|
|
|
if ctx.Msg.Chat.Type != tgapi.ChatTypePrivate {
|
|
return AsUserError(errors.New("this action is only available in private chat"))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RequireGroupChat allows execution only in group or supergroup chats.
|
|
func RequireGroupChat[T AppData]() Policy[T] {
|
|
return func(ctx *MsgContext, data T) error {
|
|
if ctx.Msg == nil || ctx.Msg.Chat == nil {
|
|
return AsInternalError(errors.New("group-chat policy requires message chat context"))
|
|
}
|
|
|
|
if ctx.Msg.Chat.Type != tgapi.ChatTypeGroup && ctx.Msg.Chat.Type != tgapi.ChatTypeSupergroup {
|
|
return AsUserError(errors.New("this action is only available in group chats"))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RequireSupergroupChat allows execution only in supergroup chats.
|
|
func RequireSupergroupChat[T AppData]() Policy[T] {
|
|
return func(ctx *MsgContext, data T) error {
|
|
if ctx.Msg == nil || ctx.Msg.Chat == nil {
|
|
return AsInternalError(errors.New("supergroup-chat policy requires message chat context"))
|
|
}
|
|
|
|
if ctx.Msg.Chat.Type != tgapi.ChatTypeSupergroup {
|
|
return AsUserError(errors.New("this action is only available in supergroup chats"))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RequireChatAdmin allows execution only for chat administrators or owners.
|
|
func RequireChatAdmin[T AppData]() Policy[T] {
|
|
return func(ctx *MsgContext, data T) error {
|
|
if ctx.FromID == 0 || ctx.ChatID == 0 {
|
|
return AsInternalError(errors.New("chat-admin policy requires message chat context"))
|
|
}
|
|
|
|
member, err := ctx.Api.GetChatMember(tgapi.GetChatMemberP{
|
|
ChatID: ctx.ChatID,
|
|
UserID: ctx.FromID,
|
|
})
|
|
if err != nil {
|
|
return AsInternalError(fmt.Errorf("failed to fetch chat member status: %w", err))
|
|
}
|
|
|
|
if member.Status != tgapi.ChatMemberStatusAdministrator && member.Status != tgapi.ChatMemberStatusOwner {
|
|
return AsUserError(errors.New("this action is only available to chat admins"))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RequireChatCreator allows execution only for the chat owner.
|
|
func RequireChatCreator[T AppData]() Policy[T] {
|
|
return func(ctx *MsgContext, data T) error {
|
|
if ctx.FromID == 0 || ctx.ChatID == 0 {
|
|
return AsInternalError(errors.New("chat-creator policy requires message chat context"))
|
|
}
|
|
|
|
member, err := ctx.Api.GetChatMember(tgapi.GetChatMemberP{
|
|
ChatID: ctx.ChatID,
|
|
UserID: ctx.FromID,
|
|
})
|
|
if err != nil {
|
|
return AsInternalError(fmt.Errorf("failed to fetch chat creator: %w", err))
|
|
}
|
|
|
|
if member.Status != tgapi.ChatMemberStatusOwner {
|
|
return AsUserError(errors.New("this action is only available to the chat creator"))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RequireBotAdmin allows execution only when the bot is an admin in the chat.
|
|
func RequireBotAdmin[T AppData]() Policy[T] {
|
|
return func(ctx *MsgContext, data T) error {
|
|
if ctx.ChatID == 0 {
|
|
return AsInternalError(errors.New("bot-admin policy requires message chat context"))
|
|
}
|
|
|
|
bot, err := ctx.Api.GetMe()
|
|
if err != nil {
|
|
return AsInternalError(fmt.Errorf("failed to fetch bot info: %w", err))
|
|
}
|
|
|
|
member, err := ctx.Api.GetChatMember(tgapi.GetChatMemberP{
|
|
ChatID: ctx.ChatID,
|
|
UserID: bot.ID,
|
|
})
|
|
if err != nil {
|
|
return AsInternalError(fmt.Errorf("failed to fetch bot member status: %w", err))
|
|
}
|
|
|
|
if member.Status != tgapi.ChatMemberStatusAdministrator && member.Status != tgapi.ChatMemberStatusOwner {
|
|
return AsUserError(errors.New("this action requires the bot to be an admin in the chat"))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RequireCallbackFromUser allows execution only for callback queries sent by non-bot users.
|
|
func RequireCallbackFromUser[T AppData]() Policy[T] {
|
|
return func(ctx *MsgContext, data T) error {
|
|
if ctx.Update.CallbackQuery == nil {
|
|
return AsInternalError(errors.New("callback-user policy requires callback query context"))
|
|
}
|
|
if ctx.Update.CallbackQuery.From.IsBot {
|
|
return AsUserError(errors.New("this action is only available to human users"))
|
|
}
|
|
return nil
|
|
}
|
|
}
|