Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a conversation rather than pretending it is an endpoint, creates channels, claims usernames with fallbacks, and grants post_messages. internal/apply sequences it: bot before administrator, test channel before public, and state saved after every step that changed the world -- a channel that exists but is unrecorded is worse than one that does not exist, because the next run creates a second. The operator session lives in OpenBao, not on disk. gotd's FileStorage would leave a full-account credential in the working directory, where it outlives the run and can be committed by accident. The bot token goes straight from BotFather's reply to OpenBao and is cleared from memory; if that write fails the error says how to recover by hand and warns against re-running, since a retry creates a second bot. Closes T05: the redaction salt is create-if-absent with no overwrite path, and the test asserts it, because rotating it invalidates every longitudinal comparison with no visible failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172sgCZEEDJcnQmr4SGDvKa Assistant: claude-code Assistant-Model: opus Assistant-Process: 1361245@bnt-lap001 Assistant-Session: b3b428ef-f3e6-4688-b091-01f71461d66a
This commit is contained in:
parent
10018a99b6
commit
5ddfee8250
14 changed files with 1544 additions and 57 deletions
50
internal/tg/live.go
Normal file
50
internal/tg/live.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package tg
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/tegwick/fluid-telegram/internal/state"
|
||||
)
|
||||
|
||||
// Live implements plan.Live over a connected client, so that a plan is computed
|
||||
// against what is actually there rather than against the state file's memory of
|
||||
// it. Everything it reports is observed; nothing is assumed.
|
||||
type Live struct {
|
||||
Ctx context.Context
|
||||
Client *Client
|
||||
Resolved *state.Resolved
|
||||
}
|
||||
|
||||
func (l Live) BotExists(username string) (bool, error) {
|
||||
if username == "" {
|
||||
return false, nil
|
||||
}
|
||||
_, err := l.Client.API().ContactsResolveUsername(l.Ctx, resolveReq(username))
|
||||
if err != nil {
|
||||
if isNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (l Live) ChannelAdminRights(chatID int64) ([]string, error) {
|
||||
ch, err := l.channel(chatID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l.Client.AdminRights(l.Ctx, ch, l.Resolved.Bot.ID)
|
||||
}
|
||||
|
||||
func (l Live) ChannelUsername(chatID int64) (string, error) {
|
||||
ch, err := l.channel(chatID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return l.Client.ChannelUsername(l.Ctx, ch)
|
||||
}
|
||||
|
||||
func (l Live) channel(chatID int64) (Channel, error) {
|
||||
return l.Client.ResolveChannel(l.Ctx, chatID)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue