chore: implement memo property runner

This commit is contained in:
Steven
2024-08-20 08:07:48 +08:00
parent f4d6675363
commit d1280bc04f
6 changed files with 216 additions and 131 deletions

View File

@@ -0,0 +1,120 @@
package memoproperty
import (
"context"
"log/slog"
"slices"
"time"
"github.com/pkg/errors"
"github.com/usememos/gomark/ast"
"github.com/usememos/gomark/parser"
"github.com/usememos/gomark/parser/tokenizer"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
type Runner struct {
Store *store.Store
}
func NewRunner(store *store.Store) *Runner {
return &Runner{
Store: store,
}
}
// Schedule runner every 12 hours.
const runnerInterval = time.Hour * 12
func (r *Runner) Run(ctx context.Context) {
ticker := time.NewTicker(runnerInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
r.RunOnce(ctx)
case <-ctx.Done():
return
}
}
}
func (r *Runner) RunOnce(ctx context.Context) {
emptyPayload := "{}"
memos, err := r.Store.ListMemos(ctx, &store.FindMemo{
PayloadFind: &store.FindMemoPayload{
Raw: &emptyPayload,
},
})
if err != nil {
slog.Error("failed to list memos", "err", err)
return
}
for _, memo := range memos {
property, err := GetMemoPropertyFromContent(memo.Content)
if err != nil {
slog.Error("failed to get memo property", "err", err)
continue
}
memo.Payload.Property = property
if err := r.Store.UpdateMemo(ctx, &store.UpdateMemo{
ID: memo.ID,
Payload: memo.Payload,
}); err != nil {
slog.Error("failed to update memo", "err", err)
}
}
}
func GetMemoPropertyFromContent(content string) (*storepb.MemoPayload_Property, error) {
nodes, err := parser.Parse(tokenizer.Tokenize(content))
if err != nil {
return nil, errors.Wrap(err, "failed to parse content")
}
property := &storepb.MemoPayload_Property{}
TraverseASTNodes(nodes, func(node ast.Node) {
switch n := node.(type) {
case *ast.Tag:
tag := n.Content
if !slices.Contains(property.Tags, tag) {
property.Tags = append(property.Tags, tag)
}
case *ast.Link, *ast.AutoLink:
property.HasLink = true
case *ast.TaskList:
property.HasTaskList = true
if !n.Complete {
property.HasIncompleteTasks = true
}
case *ast.Code, *ast.CodeBlock:
property.HasCode = true
}
})
return property, nil
}
func TraverseASTNodes(nodes []ast.Node, fn func(ast.Node)) {
for _, node := range nodes {
fn(node)
switch n := node.(type) {
case *ast.Paragraph:
TraverseASTNodes(n.Children, fn)
case *ast.Heading:
TraverseASTNodes(n.Children, fn)
case *ast.Blockquote:
TraverseASTNodes(n.Children, fn)
case *ast.OrderedList:
TraverseASTNodes(n.Children, fn)
case *ast.UnorderedList:
TraverseASTNodes(n.Children, fn)
case *ast.TaskList:
TraverseASTNodes(n.Children, fn)
case *ast.Bold:
TraverseASTNodes(n.Children, fn)
}
}
}

View File

@@ -0,0 +1,108 @@
package s3presign
import (
"context"
"log/slog"
"time"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/usememos/memos/plugin/storage/s3"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
type Runner struct {
Store *store.Store
}
func NewRunner(store *store.Store) *Runner {
return &Runner{
Store: store,
}
}
// Schedule runner every 12 hours.
const runnerInterval = time.Hour * 12
func (r *Runner) Run(ctx context.Context) {
ticker := time.NewTicker(runnerInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
r.RunOnce(ctx)
case <-ctx.Done():
return
}
}
}
func (r *Runner) RunOnce(ctx context.Context) {
r.CheckAndPresign(ctx)
}
func (r *Runner) CheckAndPresign(ctx context.Context) {
workspaceStorageSetting, err := r.Store.GetWorkspaceStorageSetting(ctx)
if err != nil {
return
}
s3StorageType := storepb.ResourceStorageType_S3
resources, err := r.Store.ListResources(ctx, &store.FindResource{
GetBlob: false,
StorageType: &s3StorageType,
})
if err != nil {
return
}
for _, resource := range resources {
s3ObjectPayload := resource.Payload.GetS3Object()
if s3ObjectPayload == nil {
continue
}
if s3ObjectPayload.LastPresignedTime != nil {
// Skip if the presigned URL is still valid for the next 4 days.
// The expiration time is set to 5 days.
if time.Now().Before(s3ObjectPayload.LastPresignedTime.AsTime().Add(4 * 24 * time.Hour)) {
continue
}
}
s3Config := workspaceStorageSetting.GetS3Config()
if s3ObjectPayload.S3Config != nil {
s3Config = s3ObjectPayload.S3Config
}
if s3Config == nil {
slog.Error("S3 config is not found")
continue
}
s3Client, err := s3.NewClient(ctx, s3Config)
if err != nil {
slog.Error("Failed to create S3 client", "error", err)
continue
}
presignURL, err := s3Client.PresignGetObject(ctx, s3ObjectPayload.Key)
if err != nil {
return
}
s3ObjectPayload.S3Config = s3Config
s3ObjectPayload.LastPresignedTime = timestamppb.New(time.Now())
if err := r.Store.UpdateResource(ctx, &store.UpdateResource{
ID: resource.ID,
Reference: &presignURL,
Payload: &storepb.ResourcePayload{
Payload: &storepb.ResourcePayload_S3Object_{
S3Object: s3ObjectPayload,
},
},
}); err != nil {
return
}
}
}

View File

@@ -0,0 +1,140 @@
// Packge version provides a runner to check the latest version of the application.
package version
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/pkg/errors"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/server/version"
"github.com/usememos/memos/store"
)
type Runner struct {
Store *store.Store
Profile *profile.Profile
}
func NewRunner(store *store.Store, profile *profile.Profile) *Runner {
return &Runner{
Store: store,
Profile: profile,
}
}
// Schedule checker every 8 hours.
const runnerInterval = time.Hour * 8
func (r *Runner) Run(ctx context.Context) {
ticker := time.NewTicker(runnerInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
r.RunOnce(ctx)
case <-ctx.Done():
return
}
}
}
func (r *Runner) RunOnce(ctx context.Context) {
r.Check(ctx)
}
func (r *Runner) Check(ctx context.Context) {
latestVersion, err := r.GetLatestVersion()
if err != nil {
return
}
if !version.IsVersionGreaterThan(latestVersion, version.GetCurrentVersion(r.Profile.Mode)) {
return
}
versionUpdateActivityType := store.ActivityTypeVersionUpdate
list, err := r.Store.ListActivities(ctx, &store.FindActivity{
Type: &versionUpdateActivityType,
})
if err != nil {
return
}
shouldNotify := true
if len(list) > 0 {
latestVersionUpdateActivity := list[0]
if latestVersionUpdateActivity.Payload != nil && version.IsVersionGreaterOrEqualThan(latestVersionUpdateActivity.Payload.VersionUpdate.Version, latestVersion) {
shouldNotify = false
}
}
if !shouldNotify {
return
}
// Create version update activity and inbox message.
activity := &store.Activity{
CreatorID: store.SystemBotID,
Type: store.ActivityTypeVersionUpdate,
Level: store.ActivityLevelInfo,
Payload: &storepb.ActivityPayload{
VersionUpdate: &storepb.ActivityVersionUpdatePayload{
Version: latestVersion,
},
},
}
if _, err := r.Store.CreateActivity(ctx, activity); err != nil {
return
}
hostUserRole := store.RoleHost
users, err := r.Store.ListUsers(ctx, &store.FindUser{
Role: &hostUserRole,
})
if err != nil {
return
}
if len(users) == 0 {
return
}
hostUser := users[0]
if _, err := r.Store.CreateInbox(ctx, &store.Inbox{
SenderID: store.SystemBotID,
ReceiverID: hostUser.ID,
Status: store.UNREAD,
Message: &storepb.InboxMessage{
Type: storepb.InboxMessage_VERSION_UPDATE,
ActivityId: &activity.ID,
},
}); err != nil {
fmt.Printf("failed to create inbox: %s\n", err)
}
}
func (*Runner) GetLatestVersion() (string, error) {
response, err := http.Get("https://www.usememos.com/api/version")
if err != nil {
return "", errors.Wrap(err, "failed to make http request")
}
defer response.Body.Close()
buf := &bytes.Buffer{}
_, err = buf.ReadFrom(response.Body)
if err != nil {
return "", errors.Wrap(err, "fail to read response body")
}
version := ""
if err = json.Unmarshal(buf.Bytes(), &version); err != nil {
return "", errors.Wrap(err, "fail to unmarshal get version response")
}
return version, nil
}

View File

@@ -0,0 +1,12 @@
package version
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetLatestVersion(t *testing.T) {
_, err := NewRunner(nil, nil).GetLatestVersion()
require.NoError(t, err)
}