v1.0.0 beta 12
This commit is contained in:
289
drafts.go
289
drafts.go
@@ -1,3 +1,31 @@
|
||||
// Package laniakea provides a safe, high-level interface for managing Telegram
|
||||
// message drafts using the tgapi library. It allows creating, editing, and
|
||||
// flushing drafts with automatic ID generation and optional bulk flushing.
|
||||
//
|
||||
// Drafts are designed to be ephemeral, mutable buffers that can be built up
|
||||
// incrementally and then sent as final messages. The package ensures safe
|
||||
// state management by copying entities and isolating draft contexts.
|
||||
//
|
||||
// Two draft ID generation strategies are supported:
|
||||
// - Random: Cryptographically secure random IDs (default). Ideal for distributed systems.
|
||||
// - Linear: Monotonically increasing IDs. Useful for persistence, debugging, or recovery.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// provider := laniakea.NewRandomDraftProvider(api)
|
||||
// provider.SetChat(-1001234567890, 0).SetParseMode(tgapi.ParseModeHTML)
|
||||
//
|
||||
// draft := provider.NewDraft(tgapi.ParseModeMarkdown)
|
||||
// draft.Push("*Hello*").Push(" **world**!")
|
||||
// err := draft.Flush() // Sends message and deletes draft
|
||||
// if err != nil {
|
||||
// log.Printf("Failed to send draft: %v", err)
|
||||
// }
|
||||
//
|
||||
// // Or flush all pending drafts at once:
|
||||
// err = provider.FlushAll() // Sends all drafts and clears them
|
||||
//
|
||||
// Note: Drafts are NOT thread-safe. Concurrent access requires external synchronization.
|
||||
package laniakea
|
||||
|
||||
import (
|
||||
@@ -7,38 +35,137 @@ import (
|
||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
||||
)
|
||||
|
||||
// draftIdGenerator defines an interface for generating unique draft IDs.
|
||||
type draftIdGenerator interface {
|
||||
// Next returns the next unique draft ID.
|
||||
Next() uint64
|
||||
}
|
||||
|
||||
type RandomDraftIdGenerator struct {
|
||||
draftIdGenerator
|
||||
}
|
||||
// RandomDraftIdGenerator generates draft IDs using cryptographically secure random numbers.
|
||||
// Suitable for distributed systems or when ID predictability is undesirable.
|
||||
type RandomDraftIdGenerator struct{}
|
||||
|
||||
// Next returns a random 64-bit unsigned integer.
|
||||
func (g *RandomDraftIdGenerator) Next() uint64 {
|
||||
return rand.Uint64()
|
||||
}
|
||||
|
||||
// LinearDraftIdGenerator generates draft IDs using a monotonically increasing counter.
|
||||
// Useful for debugging, persistence, or when drafts must be ordered.
|
||||
type LinearDraftIdGenerator struct {
|
||||
draftIdGenerator
|
||||
lastId atomic.Uint64
|
||||
}
|
||||
|
||||
// Next returns the next linear ID, atomically incremented.
|
||||
func (g *LinearDraftIdGenerator) Next() uint64 {
|
||||
return g.lastId.Add(1)
|
||||
}
|
||||
|
||||
// DraftProvider manages a collection of Drafts and provides methods to create and
|
||||
// configure them. It holds shared configuration (chat, parse mode, entities) and
|
||||
// a draft ID generator.
|
||||
//
|
||||
// DraftProvider is NOT thread-safe. Concurrent access from multiple goroutines
|
||||
// requires external synchronization.
|
||||
type DraftProvider struct {
|
||||
api *tgapi.API
|
||||
api *tgapi.API
|
||||
drafts map[uint64]*Draft
|
||||
generator draftIdGenerator
|
||||
|
||||
// Internal defaults — not exposed directly to users.
|
||||
chatID int64
|
||||
messageThreadID int
|
||||
parseMode tgapi.ParseMode
|
||||
entities []tgapi.MessageEntity
|
||||
|
||||
drafts map[uint64]*Draft
|
||||
generator draftIdGenerator
|
||||
}
|
||||
|
||||
// NewRandomDraftProvider creates a new DraftProvider using random draft IDs.
|
||||
//
|
||||
// The provider will use cryptographically secure random numbers for draft IDs.
|
||||
// All drafts created via this provider will have unpredictable, unique IDs.
|
||||
func NewRandomDraftProvider(api *tgapi.API) *DraftProvider {
|
||||
return &DraftProvider{
|
||||
api: api, generator: &RandomDraftIdGenerator{},
|
||||
drafts: make(map[uint64]*Draft),
|
||||
}
|
||||
}
|
||||
|
||||
// NewLinearDraftProvider creates a new DraftProvider using linear (incrementing) draft IDs.
|
||||
//
|
||||
// startValue is the initial value for the counter. Use 0 for fresh start, or a known
|
||||
// value to resume from persisted state.
|
||||
//
|
||||
// This is useful when you need to store draft IDs externally (e.g., in a database)
|
||||
// and want to reconstruct drafts after restart.
|
||||
func NewLinearDraftProvider(api *tgapi.API, startValue uint64) *DraftProvider {
|
||||
g := &LinearDraftIdGenerator{}
|
||||
g.lastId.Store(startValue)
|
||||
return &DraftProvider{
|
||||
api: api,
|
||||
generator: g,
|
||||
drafts: make(map[uint64]*Draft),
|
||||
}
|
||||
}
|
||||
|
||||
// SetChat sets the target chat and optional message thread for all drafts created
|
||||
// by this provider. Must be called before NewDraft().
|
||||
//
|
||||
// If not set, NewDraft() will create drafts with zero chatID, which will cause
|
||||
// SendMessageDraft to fail. Use this method to avoid runtime errors.
|
||||
func (p *DraftProvider) SetChat(chatID int64, messageThreadID int) *DraftProvider {
|
||||
p.chatID = chatID
|
||||
p.messageThreadID = messageThreadID
|
||||
return p
|
||||
}
|
||||
|
||||
// SetParseMode sets the default parse mode for all new drafts.
|
||||
// Overrides the parse mode passed to NewDraft() only if not specified there.
|
||||
func (p *DraftProvider) SetParseMode(mode tgapi.ParseMode) *DraftProvider {
|
||||
p.parseMode = mode
|
||||
return p
|
||||
}
|
||||
|
||||
// SetEntities sets the default message entities (e.g., bold, links, mentions)
|
||||
// to be copied into every new draft.
|
||||
//
|
||||
// Entities are shallow-copied — if you mutate the slice later, it will affect
|
||||
// future drafts. For safety, pass a copy if needed.
|
||||
func (p *DraftProvider) SetEntities(entities []tgapi.MessageEntity) *DraftProvider {
|
||||
p.entities = entities
|
||||
return p
|
||||
}
|
||||
|
||||
// GetDraft retrieves a draft by its ID.
|
||||
//
|
||||
// Returns the draft and true if found, or nil and false if not found.
|
||||
func (p *DraftProvider) GetDraft(id uint64) (*Draft, bool) {
|
||||
draft, ok := p.drafts[id]
|
||||
return draft, ok
|
||||
}
|
||||
|
||||
// FlushAll sends all pending drafts as final messages and clears them.
|
||||
//
|
||||
// If any draft fails to send, FlushAll returns the error immediately and
|
||||
// leaves other drafts unflushed. This allows for retry logic or logging.
|
||||
//
|
||||
// After successful flush, each draft is removed from the provider and cleared.
|
||||
func (p *DraftProvider) FlushAll() error {
|
||||
var lastErr error
|
||||
for _, draft := range p.drafts {
|
||||
if err := draft.Flush(); err != nil {
|
||||
lastErr = err
|
||||
break // Stop on first error to avoid partial state
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// Draft represents a single message draft that can be edited and flushed.
|
||||
//
|
||||
// Drafts are safe to use from a single goroutine. Multiple goroutines must
|
||||
// synchronize access manually.
|
||||
//
|
||||
// Drafts are automatically removed from the provider's map when Flush() succeeds.
|
||||
type Draft struct {
|
||||
api *tgapi.API
|
||||
provider *DraftProvider
|
||||
@@ -52,69 +179,98 @@ type Draft struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func NewRandomDraftProvider(api *tgapi.API) *DraftProvider {
|
||||
return &DraftProvider{
|
||||
api: api, generator: &RandomDraftIdGenerator{},
|
||||
parseMode: tgapi.ParseMDV2,
|
||||
drafts: make(map[uint64]*Draft),
|
||||
// NewDraft creates a new draft with the provided parse mode.
|
||||
//
|
||||
// The draft inherits the provider's chatID, messageThreadID, and entities.
|
||||
// If parseMode is zero, the provider's default parseMode is used.
|
||||
//
|
||||
// Panics if chatID is zero — call SetChat() on the provider first.
|
||||
func (p *DraftProvider) NewDraft(parseMode tgapi.ParseMode) *Draft {
|
||||
if p.chatID == 0 {
|
||||
panic("laniakea: DraftProvider.SetChat() must be called before NewDraft()")
|
||||
}
|
||||
}
|
||||
func NewLinearDraftProvider(api *tgapi.API, startValue uint64) *DraftProvider {
|
||||
g := &LinearDraftIdGenerator{}
|
||||
g.lastId.Store(startValue)
|
||||
return &DraftProvider{
|
||||
api: api,
|
||||
generator: g,
|
||||
drafts: make(map[uint64]*Draft),
|
||||
}
|
||||
}
|
||||
func (d *DraftProvider) NewDraft() *Draft {
|
||||
id := d.generator.Next()
|
||||
entitiesCopy := make([]tgapi.MessageEntity, 0)
|
||||
copy(entitiesCopy, d.entities)
|
||||
|
||||
id := p.generator.Next()
|
||||
draft := &Draft{
|
||||
api: d.api,
|
||||
provider: d,
|
||||
chatID: d.chatID,
|
||||
messageThreadID: d.messageThreadID,
|
||||
parseMode: d.parseMode,
|
||||
entities: entitiesCopy,
|
||||
api: p.api,
|
||||
provider: p,
|
||||
chatID: p.chatID,
|
||||
messageThreadID: p.messageThreadID,
|
||||
parseMode: parseMode,
|
||||
entities: p.entities, // Shallow copy — caller must ensure immutability
|
||||
ID: id,
|
||||
Message: "",
|
||||
}
|
||||
d.drafts[id] = draft
|
||||
p.drafts[id] = draft
|
||||
return draft
|
||||
}
|
||||
func (d *Draft) push(text string, escapeMd bool) error {
|
||||
if escapeMd {
|
||||
d.Message += EscapeMarkdownV2(text)
|
||||
} else {
|
||||
d.Message += text
|
||||
}
|
||||
params := tgapi.SendMessageDraftP{
|
||||
ChatID: d.chatID,
|
||||
DraftID: d.ID,
|
||||
Text: d.Message,
|
||||
ParseMode: d.parseMode,
|
||||
Entities: d.entities,
|
||||
}
|
||||
if d.messageThreadID > 0 {
|
||||
params.MessageThreadID = d.messageThreadID
|
||||
}
|
||||
_, err := d.api.SendMessageDraft(params)
|
||||
return err
|
||||
|
||||
// SetChat overrides the draft's target chat and message thread.
|
||||
//
|
||||
// This is useful for sending a draft to a different chat than the provider's default.
|
||||
func (d *Draft) SetChat(chatID int64, messageThreadID int) *Draft {
|
||||
d.chatID = chatID
|
||||
d.messageThreadID = messageThreadID
|
||||
return d
|
||||
}
|
||||
|
||||
// SetEntities replaces the draft's message entities.
|
||||
//
|
||||
// Entities are stored by reference. If you plan to mutate the slice later,
|
||||
// pass a copy: `SetEntities(append([]tgapi.MessageEntity{}, myEntities...))`
|
||||
func (d *Draft) SetEntities(entities []tgapi.MessageEntity) *Draft {
|
||||
d.entities = entities
|
||||
return d
|
||||
}
|
||||
|
||||
// Push appends text to the draft and attempts to update the server-side draft.
|
||||
//
|
||||
// Returns an error if the Telegram API rejects the update (e.g., due to network issues).
|
||||
// The draft's Message field is always updated, even if the API call fails.
|
||||
//
|
||||
// Use this method to build the message incrementally.
|
||||
func (d *Draft) Push(text string) error {
|
||||
return d.push(text, true)
|
||||
}
|
||||
func (d *Draft) PushMarkdown(text string) error {
|
||||
return d.push(text, false)
|
||||
return d.push(text)
|
||||
}
|
||||
|
||||
// GetMessage returns the current content of the draft.
|
||||
//
|
||||
// Useful for inspection, logging, or validation before flushing.
|
||||
func (d *Draft) GetMessage() string {
|
||||
return d.Message
|
||||
}
|
||||
|
||||
// Clear resets the draft's message content to empty string.
|
||||
//
|
||||
// Does not affect server-side draft — use Flush() for that.
|
||||
func (d *Draft) Clear() {
|
||||
d.Message = ""
|
||||
}
|
||||
|
||||
// Delete removes the draft from its provider and clears its content.
|
||||
//
|
||||
// This is an internal method used by Flush(). You may call it manually if you
|
||||
// want to cancel a draft without sending it.
|
||||
func (d *Draft) Delete() {
|
||||
if d.provider != nil {
|
||||
delete(d.provider.drafts, d.ID)
|
||||
}
|
||||
d.Clear()
|
||||
}
|
||||
|
||||
// Flush sends the draft as a final message and clears it locally.
|
||||
//
|
||||
// If successful:
|
||||
// - The message is sent to Telegram.
|
||||
// - The draft's content is cleared.
|
||||
// - The draft is removed from the provider's map.
|
||||
//
|
||||
// If an error occurs:
|
||||
// - The message is NOT sent.
|
||||
// - The draft remains in the provider and retains its content.
|
||||
// - You can call Flush() again to retry.
|
||||
//
|
||||
// If the draft is empty, Flush() returns nil without calling the API.
|
||||
func (d *Draft) Flush() error {
|
||||
if d.Message == "" {
|
||||
return nil
|
||||
@@ -129,10 +285,27 @@ func (d *Draft) Flush() error {
|
||||
if d.messageThreadID > 0 {
|
||||
params.MessageThreadID = d.messageThreadID
|
||||
}
|
||||
|
||||
_, err := d.api.SendMessage(params)
|
||||
if err == nil {
|
||||
d.Clear()
|
||||
delete(d.provider.drafts, d.ID)
|
||||
d.Delete()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// push is the internal helper for Push(). It updates the server draft via SendMessageDraft.
|
||||
func (d *Draft) push(text string) error {
|
||||
d.Message += text
|
||||
params := tgapi.SendMessageDraftP{
|
||||
ChatID: d.chatID,
|
||||
DraftID: d.ID,
|
||||
Text: d.Message,
|
||||
ParseMode: d.parseMode,
|
||||
Entities: d.entities,
|
||||
}
|
||||
if d.messageThreadID > 0 {
|
||||
params.MessageThreadID = d.messageThreadID
|
||||
}
|
||||
_, err := d.api.SendMessageDraft(params)
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user