1
0
Fork 0
mirror of https://github.com/anyproto/anytype-heart.git synced 2025-06-08 05:47:07 +09:00
anytype-heart/util/builtinobjects/builtinobjects.go

386 lines
11 KiB
Go

package builtinobjects
import (
"archive/zip"
"bytes"
"context"
_ "embed"
"fmt"
"io"
"io/ioutil"
"path/filepath"
"strings"
"time"
"github.com/textileio/go-threads/core/thread"
sb "github.com/anytypeio/go-anytype-middleware/core/block/editor/smartblock"
"github.com/anytypeio/any-sync/app"
"github.com/gogo/protobuf/types"
"github.com/anytypeio/go-anytype-middleware/core/anytype/config"
"github.com/anytypeio/go-anytype-middleware/core/block"
"github.com/anytypeio/go-anytype-middleware/core/block/editor/state"
"github.com/anytypeio/go-anytype-middleware/core/block/history"
"github.com/anytypeio/go-anytype-middleware/core/block/simple"
"github.com/anytypeio/go-anytype-middleware/core/block/simple/bookmark"
"github.com/anytypeio/go-anytype-middleware/core/block/simple/link"
"github.com/anytypeio/go-anytype-middleware/core/block/simple/relation"
"github.com/anytypeio/go-anytype-middleware/core/block/simple/text"
"github.com/anytypeio/go-anytype-middleware/core/block/source"
relation2 "github.com/anytypeio/go-anytype-middleware/core/relation"
"github.com/anytypeio/go-anytype-middleware/core/relation/relationutils"
"github.com/anytypeio/go-anytype-middleware/pb"
"github.com/anytypeio/go-anytype-middleware/pkg/lib/bundle"
"github.com/anytypeio/go-anytype-middleware/pkg/lib/core/smartblock"
coresb "github.com/anytypeio/go-anytype-middleware/pkg/lib/core/smartblock"
"github.com/anytypeio/go-anytype-middleware/pkg/lib/localstore/addr"
"github.com/anytypeio/go-anytype-middleware/pkg/lib/logging"
"github.com/anytypeio/go-anytype-middleware/pkg/lib/pb/model"
"github.com/anytypeio/go-anytype-middleware/util/pbtypes"
)
const CName = "builtinobjects"
//go:embed data/bundled_objects.zip
var objectsZip []byte
var log = logging.Logger("anytype-mw-builtinobjects")
const (
analyticsContext = "get-started"
builtInDashboardObjectID = "bafybajhnav5nrikgey5hb6rwiq6j6mulyon3my4ehg3riia37cape4ru"
injectionTimeout = 30 * time.Second
)
func New() BuiltinObjects {
return new(builtinObjects)
}
type BuiltinObjects interface {
app.ComponentRunnable
}
type builtinObjects struct {
cancel func()
source source.Service
service *block.Service
relService relation2.Service
createBuiltinObjects bool
idsMap map[string]string
}
func (b *builtinObjects) Init(a *app.App) (err error) {
b.source = a.MustComponent(source.CName).(source.Service)
b.service = a.MustComponent(block.CName).(*block.Service)
b.createBuiltinObjects = a.MustComponent(config.CName).(*config.Config).CreateBuiltinObjects
b.relService = a.MustComponent(relation2.CName).(relation2.Service)
b.cancel = func() {}
return
}
func (b *builtinObjects) Name() (name string) {
return CName
}
func (b *builtinObjects) Run(context.Context) (err error) {
if !b.createBuiltinObjects {
// import only for new accounts
return
}
var ctx context.Context
ctx, b.cancel = context.WithCancel(context.Background())
start := time.Now()
err = b.inject(ctx)
if err != nil {
log.Errorf("failed to import builtinObjects: %s", err.Error())
}
spent := time.Now().Sub(start)
if spent > injectionTimeout {
log.Debugf("built-in objects injection time exceeded timeout of %s and is %s", injectionTimeout.String(), spent.String())
}
return
}
func (b *builtinObjects) inject(ctx context.Context) (err error) {
zr, err := zip.NewReader(bytes.NewReader(objectsZip), int64(len(objectsZip)))
if err != nil {
return
}
b.idsMap = make(map[string]string, len(zr.File))
isSpaceDashboardIDFound := false
for _, zf := range zr.File {
id := strings.TrimSuffix(zf.Name, filepath.Ext(zf.Name))
sbt, err := smartblock.SmartBlockTypeFromID(id)
if err != nil {
sbt, err = SmartBlockTypeFromThreadID(id)
if err != nil {
return err
}
}
if sbt == smartblock.SmartBlockTypeSubObject {
// todo: probably subobjects are broken here
// preserve original id for subobjects, it makes no sense to replace them and also it breaks the grouping
b.idsMap[id] = id
continue
}
if id == builtInDashboardObjectID {
b.idsMap[id], err = b.service.GetSpaceDashboardID(ctx)
if err != nil {
return err
}
isSpaceDashboardIDFound = true
continue
}
// create object
obj, err := b.service.CreateTreeObject(ctx, sbt, func(id string) *sb.InitContext {
return &sb.InitContext{
IsNewObject: true,
Ctx: ctx,
}
})
if err != nil {
return err
}
newId := obj.Id()
b.idsMap[id] = newId
}
if !isSpaceDashboardIDFound {
panic("Workspace dashboard object file was not found in built-in objects")
}
for _, zf := range zr.File {
rd, e := zf.Open()
if e != nil {
return e
}
if err = b.createObject(ctx, rd); err != nil {
return
}
}
return nil
}
func (b *builtinObjects) createObject(ctx context.Context, rd io.ReadCloser) (err error) {
defer rd.Close()
data, err := ioutil.ReadAll(rd)
snapshot := &pb.ChangeSnapshot{}
if err = snapshot.Unmarshal(data); err != nil {
return
}
isFavorite := pbtypes.GetBool(snapshot.Data.Details, bundle.RelationKeyIsFavorite.String())
isArchived := pbtypes.GetBool(snapshot.Data.Details, bundle.RelationKeyIsArchived.String())
if isArchived {
return fmt.Errorf("object has isarchived == true")
}
st := state.NewDocFromSnapshot("", snapshot).(*state.State)
oldId := st.RootId()
newId, exists := b.idsMap[oldId]
if !exists {
return fmt.Errorf("new id not found for '%s'", st.RootId())
}
st.SetRootId(newId)
a := st.Get(newId)
m := a.Model()
sbt, err := smartblock.SmartBlockTypeFromID(newId)
if sbt == smartblock.SmartBlockTypeSubObject {
ot, err := bundle.TypeKeyFromUrl(pbtypes.GetString(st.CombinedDetails(), bundle.RelationKeyType.String()))
if err != nil {
return err
}
_, _, err = b.service.CreateObject(&pb.RpcObjectCreateRequest{Details: st.CombinedDetails()}, ot)
if err != nil {
return err
}
}
f := m.GetFields().GetFields()
if f == nil {
f = make(map[string]*types.Value)
}
m.Fields = &types.Struct{Fields: f}
f["analyticsContext"] = pbtypes.String(analyticsContext)
if f["analyticsOriginalId"] == nil {
// in case we already have analyticsOriginalId do not update it
f["analyticsOriginalId"] = pbtypes.String(oldId)
}
st.Set(simple.New(m))
rels := relationutils.MigrateRelationModels(st.OldExtraRelations())
st.AddRelationLinks(rels...)
st.RemoveDetail(bundle.RelationKeyCreator.String(), bundle.RelationKeyLastModifiedBy.String(), bundle.RelationKeyLastOpenedDate.String(), bundle.RelationKeyLinks.String())
st.SetLocalDetail(bundle.RelationKeyCreator.String(), pbtypes.String(addr.AnytypeProfileId))
st.SetLocalDetail(bundle.RelationKeyLastModifiedBy.String(), pbtypes.String(addr.AnytypeProfileId))
st.SetLocalDetail(bundle.RelationKeyWorkspaceId.String(), pbtypes.String(b.service.Anytype().PredefinedBlocks().Account))
st.InjectDerivedDetails()
if err = b.validate(st); err != nil {
return
}
st.Iterate(func(bl simple.Block) (isContinue bool) {
switch a := bl.(type) {
case link.Block:
newTarget := b.idsMap[a.Model().GetLink().TargetBlockId]
if newTarget == "" {
// maybe we should panic here?
log.With("object", st.RootId()).Errorf("cant find target id for link: %s", a.Model().GetLink().TargetBlockId)
return true
}
a.Model().GetLink().TargetBlockId = newTarget
st.Set(simple.New(a.Model()))
case bookmark.Block:
newTarget := b.idsMap[a.Model().GetBookmark().TargetObjectId]
if newTarget == "" {
// maybe we should panic here?
log.With("object", oldId).Errorf("cant find target id for bookmark: %s", a.Model().GetBookmark().TargetObjectId)
return true
}
a.Model().GetBookmark().TargetObjectId = newTarget
st.Set(simple.New(a.Model()))
case text.Block:
for i, mark := range a.Model().GetText().GetMarks().GetMarks() {
if mark.Type != model.BlockContentTextMark_Mention && mark.Type != model.BlockContentTextMark_Object {
continue
}
newTarget := b.idsMap[mark.Param]
if newTarget == "" {
log.With("object", oldId).Errorf("cant find target id for mention: %s", mark.Param)
continue
}
a.Model().GetText().GetMarks().GetMarks()[i].Param = newTarget
}
st.Set(simple.New(a.Model()))
}
return true
})
for k, v := range st.Details().GetFields() {
rel, err := bundle.GetRelation(bundle.RelationKey(k))
if err != nil {
log.With("object", oldId).Errorf("failed to find relation %s: %s", k, err.Error())
continue
}
if rel.Format != model.RelationFormat_object && rel.Format != model.RelationFormat_tag && rel.Format != model.RelationFormat_status {
continue
}
vals := pbtypes.GetStringListValue(v)
for i, val := range vals {
if bundle.HasRelation(val) {
continue
}
newTarget, _ := b.idsMap[val]
if newTarget == "" {
log.With("object", oldId).Errorf("cant find target id for relation %s: %s", k, val)
continue
}
vals[i] = newTarget
}
st.SetDetail(k, pbtypes.StringList(vals))
}
start := time.Now()
err = b.service.Do(newId, func(sb sb.SmartBlock) error {
return history.ResetToVersion(sb, st)
})
if err != nil {
return err
}
err = b.service.Do(newId, func(b sb.SmartBlock) error {
return nil
})
log.With("timeMs", time.Now().Sub(start).Milliseconds()).Info("creating debug obj")
if isFavorite {
err = b.service.SetPageIsFavorite(pb.RpcObjectSetIsFavoriteRequest{ContextId: newId, IsFavorite: true})
if err != nil {
log.Errorf("failed to set isFavorite when importing object %s(originally %s): %s", newId, oldId, err.Error())
}
}
return err
}
func (b *builtinObjects) validate(st *state.State) (err error) {
var relKeys []string
for _, rel := range st.PickRelationLinks() {
if !bundle.HasRelation(rel.Key) {
// todo: temporarily, make this as error
log.Errorf("builtin objects should not contain custom relations, got %s in %s(%s)", rel.Key, st.RootId(), pbtypes.GetString(st.Details(), bundle.RelationKeyName.String()))
}
}
st.Iterate(func(b simple.Block) (isContinue bool) {
if rb, ok := b.(relation.Block); ok {
relKeys = append(relKeys, rb.Model().GetRelation().Key)
}
return true
})
for _, rk := range relKeys {
if !st.HasRelation(rk) {
return fmt.Errorf("bundled template validation: relation '%v' exists in block but not in extra relations", rk)
}
}
return nil
}
func (b *builtinObjects) Close(ctx context.Context) (err error) {
if b.cancel != nil {
b.cancel()
}
return
}
func SmartBlockTypeFromThreadID(id string) (coresb.SmartBlockType, error) {
tid, err := thread.Decode(id)
if err != nil {
return coresb.SmartBlockTypePage, err
}
rawid := tid.KeyString()
// skip version
_, n := uvarint(rawid)
// skip variant
_, n2 := uvarint(rawid[n:])
blockType, _ := uvarint(rawid[n+n2:])
// checks in order to detect invalid sb type
if err := coresb.SmartBlockType(blockType).Valid(); err != nil {
return 0, err
}
return coresb.SmartBlockType(blockType), nil
}
func uvarint(buf string) (uint64, int) {
var x uint64
var s uint
// we have a binary string so we can't use a range loope
for i := 0; i < len(buf); i++ {
b := buf[i]
if b < 0x80 {
if i > 9 || i == 9 && b > 1 {
return 0, -(i + 1) // overflow
}
return x | uint64(b)<<s, i + 1
}
x |= uint64(b&0x7f) << s
s += 7
}
return 0, 0
}