Skip to content

Commit

Permalink
automod: add generic caching, and hydrate some account meta
Browse files Browse the repository at this point in the history
  • Loading branch information
bnewbold committed Nov 16, 2023
1 parent cc5b281 commit 9168140
Show file tree
Hide file tree
Showing 12 changed files with 310 additions and 41 deletions.
98 changes: 98 additions & 0 deletions automod/account_meta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package automod

import (
"context"
"encoding/json"
"fmt"
"time"

appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/atproto/identity"
)

type ProfileSummary struct {
HasAvatar bool
Description *string
DisplayName *string
}

type AccountPrivate struct {
Email string
EmailConfirmed bool
}

// information about a repo/account/identity, always pre-populated and relevant to many rules
type AccountMeta struct {
Identity *identity.Identity
Profile ProfileSummary
Private *AccountPrivate
AccountLabels []string
FollowersCount int64
FollowsCount int64
PostsCount int64
IndexedAt *time.Time
}

func (e *Engine) GetAccountMeta(ctx context.Context, ident *identity.Identity) (*AccountMeta, error) {

// wipe parsed public key; it's a waste of space and can't serialize
ident.ParsedPublicKey = nil

existing, err := e.Cache.Get(ctx, "acct", ident.DID.String())
if err != nil {
return nil, err
}
if existing != "" {
var am AccountMeta
err := json.Unmarshal([]byte(existing), &am)
if err != nil {
return nil, fmt.Errorf("parsing AccountMeta from cache: %v", err)
}
am.Identity = ident
return &am, nil
}

// fetch account metadata
pv, err := appbsky.ActorGetProfile(ctx, e.BskyClient, ident.DID.String())
if err != nil {
return nil, err
}

var labels []string
for _, lbl := range pv.Labels {
labels = append(labels, lbl.Val)
}

am := AccountMeta{
Identity: ident,
Profile: ProfileSummary{
HasAvatar: pv.Avatar != nil,
Description: pv.Description,
DisplayName: pv.DisplayName,
},
AccountLabels: dedupeStrings(labels),
}
if pv.PostsCount != nil {
am.PostsCount = *pv.PostsCount
}
if pv.FollowersCount != nil {
am.FollowersCount = *pv.FollowersCount
}
if pv.FollowsCount != nil {
am.FollowsCount = *pv.FollowsCount
}

if e.AdminClient != nil {
// XXX: get admin-level info (email, indexed at, etc). requires lexgen update
}

val, err := json.Marshal(&am)
if err != nil {
return nil, err
}

if err := e.Cache.Set(ctx, "acct", ident.DID.String(), string(val)); err != nil {
return nil, err
}
return &am, nil
}
36 changes: 36 additions & 0 deletions automod/cachestore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package automod

import (
"context"
"time"

"github.com/hashicorp/golang-lru/v2/expirable"
)

type CacheStore interface {
Get(ctx context.Context, name, key string) (string, error)
Set(ctx context.Context, name, key string, val string) error
}

type MemCacheStore struct {
Data *expirable.LRU[string, string]
}

func NewMemCacheStore(capacity int, ttl time.Duration) MemCacheStore {
return MemCacheStore{
Data: expirable.NewLRU[string, string](capacity, nil, ttl),
}
}

func (s MemCacheStore) Get(ctx context.Context, name, key string) (string, error) {
v, ok := s.Data.Get(name + "/" + key)
if !ok {
return "", nil
}
return v, nil
}

func (s MemCacheStore) Set(ctx context.Context, name, key string, val string) error {
s.Data.Add(name+"/"+key, val)
return nil
}
44 changes: 30 additions & 14 deletions automod/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ import (
//
// TODO: careful when initializing: several fields should not be null or zero, even though they are pointer type.
type Engine struct {
Logger *slog.Logger
Directory identity.Directory
Rules RuleSet
Counters CountStore
Sets SetStore
Logger *slog.Logger
Directory identity.Directory
Rules RuleSet
Counters CountStore
Sets SetStore
Cache CacheStore
BskyClient *xrpc.Client
// used to persist moderation actions in mod service (optional)
AdminClient *xrpc.Client
}
Expand All @@ -41,10 +43,14 @@ func (e *Engine) ProcessIdentityEvent(ctx context.Context, t string, did syntax.
return fmt.Errorf("identity not found for did: %s", did.String())
}

am, err := e.GetAccountMeta(ctx, ident)
if err != nil {
return err
}
evt := IdentityEvent{
Event{
Engine: e,
Account: AccountMeta{Identity: ident},
Account: *am,
},
}
if err := e.Rules.CallIdentityRules(&evt); err != nil {
Expand All @@ -62,11 +68,13 @@ func (e *Engine) ProcessIdentityEvent(ctx context.Context, t string, did syntax.

func (e *Engine) ProcessRecord(ctx context.Context, did syntax.DID, path, recCID string, rec any) error {
// similar to an HTTP server, we want to recover any panics from rule execution
/* XXX
defer func() {
if r := recover(); r != nil {
e.Logger.Error("automod event execution exception", "err", r)
}
}()
*/

ident, err := e.Directory.LookupDID(ctx, did)
if err != nil {
Expand All @@ -83,7 +91,11 @@ func (e *Engine) ProcessRecord(ctx context.Context, did syntax.DID, path, recCID
if !ok {
return fmt.Errorf("mismatch between collection (%s) and type", collection)
}
evt := e.NewPostEvent(ident, path, recCID, post)
am, err := e.GetAccountMeta(ctx, ident)
if err != nil {
return err
}
evt := e.NewPostEvent(*am, path, recCID, post)
e.Logger.Debug("processing post", "did", ident.DID, "path", path)
if err := e.Rules.CallPostRules(&evt); err != nil {
return err
Expand All @@ -99,7 +111,11 @@ func (e *Engine) ProcessRecord(ctx context.Context, did syntax.DID, path, recCID
return err
}
default:
evt := e.NewRecordEvent(ident, path, recCID, rec)
am, err := e.GetAccountMeta(ctx, ident)
if err != nil {
return err
}
evt := e.NewRecordEvent(*am, path, recCID, rec)
e.Logger.Debug("processing record", "did", ident.DID, "path", path)
if err := e.Rules.CallRecordRules(&evt); err != nil {
return err
Expand All @@ -119,14 +135,14 @@ func (e *Engine) ProcessRecord(ctx context.Context, did syntax.DID, path, recCID
return nil
}

func (e *Engine) NewPostEvent(ident *identity.Identity, path, recCID string, post *appbsky.FeedPost) PostEvent {
func (e *Engine) NewPostEvent(am AccountMeta, path, recCID string, post *appbsky.FeedPost) PostEvent {
parts := strings.SplitN(path, "/", 2)
return PostEvent{
RecordEvent{
Event{
Engine: e,
Logger: e.Logger.With("did", ident.DID, "collection", parts[0], "rkey", parts[1]),
Account: AccountMeta{Identity: ident},
Logger: e.Logger.With("did", am.Identity.DID, "collection", parts[0], "rkey", parts[1]),
Account: am,
},
parts[0],
parts[1],
Expand All @@ -140,13 +156,13 @@ func (e *Engine) NewPostEvent(ident *identity.Identity, path, recCID string, pos
}
}

func (e *Engine) NewRecordEvent(ident *identity.Identity, path, recCID string, rec any) RecordEvent {
func (e *Engine) NewRecordEvent(am AccountMeta, path, recCID string, rec any) RecordEvent {
parts := strings.SplitN(path, "/", 2)
return RecordEvent{
Event{
Engine: e,
Logger: e.Logger.With("did", ident.DID, "collection", parts[0], "rkey", parts[1]),
Account: AccountMeta{Identity: ident},
Logger: e.Logger.With("did", am.Identity.DID, "collection", parts[0], "rkey", parts[1]),
Account: am,
},
parts[0],
parts[1],
Expand Down
9 changes: 1 addition & 8 deletions automod/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,13 @@ import (

comatproto "github.com/bluesky-social/indigo/api/atproto"
appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/atproto/identity"
)

type ModReport struct {
ReasonType string
Comment string
}

// information about a repo/account/identity, always pre-populated and relevant to many rules
type AccountMeta struct {
Identity *identity.Identity
// TODO: createdAt / age
}

type CounterRef struct {
Name string
Val string
Expand Down Expand Up @@ -84,7 +77,7 @@ func (e *Event) PersistAccountActions(ctx context.Context) error {
xrpcc := e.Engine.AdminClient
if len(e.AccountLabels) > 0 {
_, err := comatproto.AdminTakeModerationAction(ctx, xrpcc, &comatproto.AdminTakeModerationAction_Input{
Action: "com.atproto.admin.defs#createLabels",
Action: "com.atproto.admin.defs#flag",
CreateLabelVals: dedupeStrings(e.AccountLabels),
Reason: "automod",
CreatedBy: xrpcc.Auth.Did,
Expand Down
63 changes: 63 additions & 0 deletions automod/redis_cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package automod

import (
"context"
"time"

"github.com/go-redis/cache/v9"
"github.com/redis/go-redis/v9"
)

type RedisCacheStore struct {
Data *cache.Cache
TTL time.Duration
}

var _ CacheStore = (*RedisCacheStore)(nil)

func NewRedisCacheStore(redisURL string, ttl time.Duration) (*RedisCacheStore, error) {
opt, err := redis.ParseURL(redisURL)
if err != nil {
return nil, err
}
rdb := redis.NewClient(opt)
// check redis connection
_, err = rdb.Ping(context.TODO()).Result()
if err != nil {
return nil, err
}
data := cache.New(&cache.Options{
Redis: rdb,
LocalCache: cache.NewTinyLFU(10_000, ttl),
})
return &RedisCacheStore{
Data: data,
TTL: ttl,
}, nil
}

func redisCacheKey(name, key string) string {
return "cache/" + name + "/" + key
}

func (s RedisCacheStore) Get(ctx context.Context, name, key string) (string, error) {
var val string
err := s.Data.Get(ctx, redisCacheKey(name, key), &val)
if err == cache.ErrCacheMiss {
return "", nil
}
if err != nil {
return "", err
}
return val, nil
}

func (s RedisCacheStore) Set(ctx context.Context, name, key string, val string) error {
s.Data.Set(&cache.Item{
Ctx: ctx,
Key: redisCacheKey(name, key),
Value: val,
TTL: s.TTL,
})
return nil
}
Loading

0 comments on commit 9168140

Please sign in to comment.