mirror of
https://github.com/kevin-DL/services.git
synced 2026-01-12 03:05:14 +00:00
222 lines
5.3 KiB
Go
222 lines
5.3 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/micro/micro/v3/service/auth"
|
|
"github.com/micro/micro/v3/service/errors"
|
|
"github.com/micro/micro/v3/service/logger"
|
|
"github.com/micro/micro/v3/service/store"
|
|
"github.com/micro/services/pkg/tenant"
|
|
pb "github.com/micro/services/seen/proto"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
var (
|
|
ErrMissingUserID = errors.BadRequest("MISSING_USER_ID", "Missing UserID")
|
|
ErrMissingResourceID = errors.BadRequest("MISSING_RESOURCE_ID", "Missing ResourceID")
|
|
ErrMissingResourceIDs = errors.BadRequest("MISSING_RESOURCE_IDS", "Missing ResourceIDs")
|
|
ErrMissingResourceType = errors.BadRequest("MISSING_RESOURCE_TYPE", "Missing ResourceType")
|
|
ErrStore = errors.InternalServerError("STORE_ERROR", "Error connecting to the store")
|
|
)
|
|
|
|
type Seen struct{}
|
|
|
|
type Record struct {
|
|
ID string
|
|
UserID string
|
|
ResourceID string
|
|
ResourceType string
|
|
Timestamp time.Time
|
|
}
|
|
|
|
func (r *Record) Key(ctx context.Context) string {
|
|
key := fmt.Sprintf("%s:%s:%s", r.UserID, r.ResourceType, r.ResourceID)
|
|
|
|
t, ok := tenant.FromContext(ctx)
|
|
if !ok {
|
|
return key
|
|
}
|
|
|
|
return fmt.Sprintf("%s/%s", t, key)
|
|
}
|
|
|
|
func (r *Record) Marshal() []byte {
|
|
b, _ := json.Marshal(r)
|
|
return b
|
|
}
|
|
|
|
func (r *Record) Unmarshal(b []byte) error {
|
|
return json.Unmarshal(b, &r)
|
|
}
|
|
|
|
// Set a resource as seen by a user. If no timestamp is provided, the current time is used.
|
|
func (s *Seen) Set(ctx context.Context, req *pb.SetRequest, rsp *pb.SetResponse) error {
|
|
_, ok := auth.AccountFromContext(ctx)
|
|
if !ok {
|
|
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
|
|
}
|
|
// validate the request
|
|
if len(req.UserId) == 0 {
|
|
return ErrMissingUserID
|
|
}
|
|
if len(req.ResourceId) == 0 {
|
|
return ErrMissingResourceID
|
|
}
|
|
if len(req.ResourceType) == 0 {
|
|
return ErrMissingResourceType
|
|
}
|
|
|
|
// default the timestamp
|
|
if req.Timestamp == nil {
|
|
req.Timestamp = timestamppb.New(time.Now())
|
|
}
|
|
|
|
// find the resource
|
|
instance := &Record{
|
|
UserID: req.UserId,
|
|
ResourceID: req.ResourceId,
|
|
ResourceType: req.ResourceType,
|
|
}
|
|
|
|
_, err := store.Read(instance.Key(ctx), store.ReadLimit(1))
|
|
if err == store.ErrNotFound {
|
|
instance.ID = uuid.New().String()
|
|
} else if err != nil {
|
|
logger.Errorf("Error with store: %v", err)
|
|
return ErrStore
|
|
}
|
|
|
|
// update the resource
|
|
instance.Timestamp = req.Timestamp.AsTime()
|
|
|
|
if err := store.Write(&store.Record{
|
|
Key: instance.Key(ctx),
|
|
Value: instance.Marshal(),
|
|
}); err != nil {
|
|
logger.Errorf("Error with store: %v", err)
|
|
return ErrStore
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Unset a resource as seen, used in cases where a user viewed a resource but wants to override
|
|
// this so they remember to action it in the future, e.g. "Mark this as unread".
|
|
func (s *Seen) Unset(ctx context.Context, req *pb.UnsetRequest, rsp *pb.UnsetResponse) error {
|
|
_, ok := auth.AccountFromContext(ctx)
|
|
if !ok {
|
|
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
|
|
}
|
|
// validate the request
|
|
if len(req.UserId) == 0 {
|
|
return ErrMissingUserID
|
|
}
|
|
if len(req.ResourceId) == 0 {
|
|
return ErrMissingResourceID
|
|
}
|
|
if len(req.ResourceType) == 0 {
|
|
return ErrMissingResourceType
|
|
}
|
|
|
|
instance := &Record{
|
|
UserID: req.UserId,
|
|
ResourceID: req.ResourceId,
|
|
ResourceType: req.ResourceType,
|
|
}
|
|
|
|
// delete the object from the store
|
|
if err := store.Delete(instance.Key(ctx)); err != nil {
|
|
logger.Errorf("Error with store: %v", err)
|
|
return ErrStore
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Read returns the timestamps at which various resources were seen by a user. If no timestamp
|
|
// is returned for a given resource_id, it indicates that resource has not yet been seen by the
|
|
// user.
|
|
func (s *Seen) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error {
|
|
_, ok := auth.AccountFromContext(ctx)
|
|
if !ok {
|
|
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
|
|
}
|
|
// validate the request
|
|
if len(req.UserId) == 0 {
|
|
return ErrMissingUserID
|
|
}
|
|
if len(req.ResourceIds) == 0 {
|
|
return ErrMissingResourceIDs
|
|
}
|
|
if len(req.ResourceType) == 0 {
|
|
return ErrMissingResourceType
|
|
}
|
|
|
|
rec := &Record{
|
|
UserID: req.UserId,
|
|
ResourceType: req.ResourceType,
|
|
}
|
|
|
|
var recs []*store.Record
|
|
var err error
|
|
|
|
// get the records for the resource type
|
|
if len(req.ResourceIds) == 1 {
|
|
// read the key itself
|
|
rec.ResourceID = req.ResourceIds[0]
|
|
// gen key
|
|
key := rec.Key(ctx)
|
|
// get the record
|
|
recs, err = store.Read(key, store.ReadLimit(1))
|
|
} else {
|
|
// create a key prefix
|
|
key := rec.Key(ctx)
|
|
// otherwise read the prefix
|
|
recs, err = store.Read(key, store.ReadPrefix())
|
|
}
|
|
|
|
if err == store.ErrNotFound {
|
|
return nil
|
|
} else if err != nil {
|
|
logger.Errorf("Error with store: %v", err)
|
|
return ErrStore
|
|
}
|
|
|
|
// make an id map
|
|
ids := make(map[string]bool)
|
|
|
|
for _, id := range req.ResourceIds {
|
|
ids[id] = true
|
|
}
|
|
|
|
// make the map
|
|
rsp.Timestamps = make(map[string]*timestamppb.Timestamp)
|
|
|
|
// range over records for the user/resource type
|
|
// TODO: add some sort of filter query in store
|
|
for _, rec := range recs {
|
|
// get id
|
|
parts := strings.Split(rec.Key, ":")
|
|
id := parts[2]
|
|
|
|
fmt.Println("checking record", rec.Key, id)
|
|
|
|
if ok := ids[id]; !ok {
|
|
continue
|
|
}
|
|
|
|
// add the timestamp for the record
|
|
r := new(Record)
|
|
r.Unmarshal(rec.Value)
|
|
rsp.Timestamps[id] = timestamppb.New(r.Timestamp)
|
|
}
|
|
|
|
return nil
|
|
}
|