Files

212 lines
5.8 KiB
Go

package mongostore
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
// deliveryTTL is the retention window for per-event delivery markers; enforced
// by the TTL index in EnsureIndexes.
const deliveryTTL = 7 * 24 * time.Hour
type PendingComponentEvent struct {
ComponentID string `bson:"_id"`
ComponentName string `bson:"componentName"`
Status string `bson:"status"`
UpdatedAt string `bson:"updatedAt"`
Position int `bson:"position"`
PreviousStatus string `bson:"previousStatus"`
DeliveryKey string `bson:"deliveryKey"`
}
func (s *Store) IsInitialized(ctx context.Context) (bool, error) {
err := s.meta.FindOne(ctx, bson.M{"_id": metaInitializedID}).Err()
if errors.Is(err, mongo.ErrNoDocuments) {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
func (s *Store) SetInitialized(ctx context.Context) error {
_, err := s.meta.UpdateOne(ctx, bson.M{"_id": metaInitializedID},
bson.M{"$set": bson.M{"value": true}}, options.UpdateOne().SetUpsert(true))
return err
}
func (s *Store) ComponentStatuses(ctx context.Context) (map[string]string, error) {
cursor, err := s.componentStatuses.Find(ctx, bson.M{})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
statuses := map[string]string{}
for cursor.Next(ctx) {
var doc struct {
ID string `bson:"_id"`
Status string `bson:"status"`
}
if err := cursor.Decode(&doc); err != nil {
return nil, err
}
statuses[doc.ID] = doc.Status
}
if err := cursor.Err(); err != nil {
return nil, err
}
return statuses, nil
}
func (s *Store) SaveComponentStatus(ctx context.Context, componentID, status string) error {
_, err := s.componentStatuses.UpdateOne(ctx, bson.M{"_id": componentID},
bson.M{"$set": bson.M{"status": status}}, options.UpdateOne().SetUpsert(true))
return err
}
func (s *Store) PendingComponentEvents(ctx context.Context) (map[string]PendingComponentEvent, error) {
cursor, err := s.pendingComponentEvents.Find(ctx, bson.M{})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
events := map[string]PendingComponentEvent{}
for cursor.Next(ctx) {
var event PendingComponentEvent
if err := cursor.Decode(&event); err != nil {
return nil, err
}
events[event.ComponentID] = event
}
if err := cursor.Err(); err != nil {
return nil, err
}
return events, nil
}
func (s *Store) SavePendingComponentEvent(ctx context.Context, event PendingComponentEvent) error {
_, err := s.pendingComponentEvents.ReplaceOne(ctx, bson.M{"_id": event.ComponentID}, event,
options.Replace().SetUpsert(true))
return err
}
func (s *Store) RemovePendingComponentEvent(ctx context.Context, componentID string) error {
_, err := s.pendingComponentEvents.DeleteOne(ctx, bson.M{"_id": componentID})
return err
}
func (s *Store) HasIncidentUpdateVersion(ctx context.Context, updateID, version string) (bool, error) {
var doc struct {
Version string `bson:"version"`
}
err := s.incidentUpdateVersions.FindOne(ctx, bson.M{"_id": updateID}).Decode(&doc)
if errors.Is(err, mongo.ErrNoDocuments) {
return false, nil
}
if err != nil {
return false, err
}
return doc.Version == version, nil
}
func (s *Store) MarkIncidentUpdateVersion(ctx context.Context, updateID, version string) error {
_, err := s.incidentUpdateVersions.UpdateOne(ctx, bson.M{"_id": updateID},
bson.M{"$set": bson.M{"version": version}}, options.UpdateOne().SetUpsert(true))
return err
}
// DeliveredSubscribers returns the subscriber keys already notified for
// eventKey. One Find replaces a per-subscriber membership check.
func (s *Store) DeliveredSubscribers(ctx context.Context, eventKey string) (map[string]bool, error) {
cursor, err := s.delivery.Find(ctx, bson.M{"eventKey": eventKey})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
delivered := map[string]bool{}
for cursor.Next(ctx) {
var doc struct {
Subscriber string `bson:"subscriber"`
}
if err := cursor.Decode(&doc); err != nil {
return nil, err
}
delivered[doc.Subscriber] = true
}
if err := cursor.Err(); err != nil {
return nil, err
}
return delivered, nil
}
func (s *Store) MarkDelivered(ctx context.Context, eventKey, subscriberKey string) error {
id := eventKey + "|" + subscriberKey
update := bson.M{"$set": bson.M{
"eventKey": eventKey,
"subscriber": subscriberKey,
"expiresAt": time.Now().Add(deliveryTTL),
}}
_, err := s.delivery.UpdateOne(ctx, bson.M{"_id": id}, update, options.UpdateOne().SetUpsert(true))
return err
}
func (s *Store) ClearDelivery(ctx context.Context, eventKey string) error {
_, err := s.delivery.DeleteMany(ctx, bson.M{"eventKey": eventKey})
return err
}
func (s *Store) TelegramOffset(ctx context.Context) (int64, error) {
var doc struct {
Value any `bson:"value"`
}
err := s.meta.FindOne(ctx, bson.M{"_id": metaTelegramOffsetID}).Decode(&doc)
if errors.Is(err, mongo.ErrNoDocuments) {
return 0, nil
}
if err != nil {
return 0, err
}
offset, ok := parseTelegramOffsetValue(doc.Value)
if ok {
return offset, nil
}
if _, err := s.meta.DeleteOne(ctx, bson.M{"_id": metaTelegramOffsetID}); err != nil {
return 0, fmt.Errorf("clear invalid telegram offset: %w", err)
}
return 0, nil
}
func parseTelegramOffsetValue(value any) (int64, bool) {
switch value := value.(type) {
case int:
return int64(value), true
case int32:
return int64(value), true
case int64:
return value, true
case string:
offset, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
return offset, err == nil
default:
return 0, false
}
}
func (s *Store) SaveTelegramOffset(ctx context.Context, offset int64) error {
_, err := s.meta.UpdateOne(ctx, bson.M{"_id": metaTelegramOffsetID},
bson.M{"$set": bson.M{"value": offset}}, options.UpdateOne().SetUpsert(true))
return err
}