added ability to copy bot token

This commit is contained in:
onysd 2026-08-05 22:33:33 +03:00
parent bb1f680d3b
commit 8aad71643f
16 changed files with 294 additions and 23 deletions

View file

@ -58,6 +58,7 @@ const (
ActionGiveGift = "gifts.give"
ActionCreateBot = "bot.create"
ActionDeleteBot = "bot.delete"
ActionExportBotToken = "bot.export_token"
ActionSetStickerSetArchived = "stickers.set_archived"
ActionSetStickerSetSortOrder = "stickers.set_sort_order"
ActionRenameStickerSet = "stickers.rename"
@ -333,6 +334,10 @@ type StickerSetsService interface {
type BotService interface {
CreateBot(ctx context.Context, ownerUserID int64, name, username string) (domain.User, string, error)
DeleteBot(ctx context.Context, botUserID int64) (domain.User, error)
// AdminExportBotToken returns a non-system bot's current token with no
// ownership check. Used by ExportBotToken; the token never enters the
// audit/replay record (see CommandResult.transientDetails).
AdminExportBotToken(ctx context.Context, botUserID int64) (string, error)
}
// EmojiService renders custom-emoji document animations for the admin emoji
@ -1027,6 +1032,11 @@ type DeleteBotRequest struct {
BotUserID int64 `json:"bot_user_id"`
}
type ExportBotTokenRequest struct {
CommandMeta
BotUserID int64 `json:"bot_user_id"`
}
// MintCollectibleUsernameRequest mints a collectible username asset. At most one
// of OwnerUserID / OwnerChannelID may be set: neither mints into the operator
// vault, one assigns the asset to that holder in the same command.
@ -2039,6 +2049,39 @@ func (s *Service) DeleteBot(ctx context.Context, req DeleteBotRequest) (CommandR
})
}
// ExportBotToken returns a non-system bot's current token (unrotated) via the
// audited runCommand wrapper. Like CreateBot's token, it travels only in
// transientDetails -- excluded from the stored/replayed command JSON so it
// never lands in audit storage. The admin console's own UI additionally never
// renders this token on screen; it copies the response straight to the
// clipboard.
func (s *Service) ExportBotToken(ctx context.Context, req ExportBotTokenRequest) (CommandResult, error) {
if s == nil || s.bots == nil {
return CommandResult{}, fmt.Errorf("admin bot dependency is not configured")
}
if req.BotUserID <= 0 {
return CommandResult{}, fmt.Errorf("bot_user_id is required")
}
if domain.IsSystemUserID(req.BotUserID) {
return CommandResult{}, fmt.Errorf("system bots have no exportable token")
}
return s.runCommand(ctx, req.CommandMeta, ActionExportBotToken, req.BotUserID, domain.Peer{}, req, func() (CommandResult, error) {
details := map[string]any{"bot_user_id": req.BotUserID}
if req.DryRun {
return CommandResult{Message: "token export validated", Details: details}, nil
}
token, err := s.bots.AdminExportBotToken(ctx, req.BotUserID)
if err != nil {
return CommandResult{Details: details}, err
}
return CommandResult{
Message: "token exported",
Details: details,
transientDetails: map[string]any{"token": token},
}, nil
})
}
// MintCollectibleUsername creates a collectible username asset and optionally
// assigns it in the same command. Shape validation runs before the command is
// journalled; occupancy is checked inside it, so a dry-run reports a taken name

View file

@ -617,6 +617,7 @@ type fakeBotService struct {
token string
createCalls int
deleteCalls int
exportCalls int
}
func (f *fakeBotService) CreateBot(_ context.Context, _ int64, name, username string) (domain.User, string, error) {
@ -629,6 +630,11 @@ func (f *fakeBotService) DeleteBot(_ context.Context, botUserID int64) (domain.U
return domain.User{ID: botUserID, Bot: true, Deleted: true}, nil
}
func (f *fakeBotService) AdminExportBotToken(_ context.Context, botUserID int64) (string, error) {
f.exportCalls++
return f.token, nil
}
type fakeRestrictionStore struct {
items map[int64]domain.AccountFreeze
setCalls int