Move Gitea feed from frontend to backend with cached GraphQL proxy
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m39s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m39s
Replaces direct browser-to-Gitea API calls with a backend service that proxies and caches the feed (1-min TTL), served via the existing GraphQL HomeData query. Commit message parsing now happens server-side. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,15 @@ type ComplexityRoot struct {
|
||||
UpdatedAt func(childComplexity int) int
|
||||
}
|
||||
|
||||
GiteaFeedItem struct {
|
||||
AvatarURL func(childComplexity int) int
|
||||
CommitMessage func(childComplexity int) int
|
||||
CreatedAt func(childComplexity int) int
|
||||
OpType func(childComplexity int) int
|
||||
RepoName func(childComplexity int) int
|
||||
RepoURL func(childComplexity int) int
|
||||
}
|
||||
|
||||
Message struct {
|
||||
AuthorID func(childComplexity int) int
|
||||
Content func(childComplexity int) int
|
||||
@@ -100,6 +109,7 @@ type ComplexityRoot struct {
|
||||
Query struct {
|
||||
Activities func(childComplexity int) int
|
||||
Favorites func(childComplexity int) int
|
||||
GiteaFeed func(childComplexity int) int
|
||||
Me func(childComplexity int) int
|
||||
Messages func(childComplexity int) int
|
||||
Post func(childComplexity int, id int) int
|
||||
@@ -197,6 +207,7 @@ type QueryResolver interface {
|
||||
Messages(ctx context.Context) ([]*models.Message, error)
|
||||
SpotifyListening(ctx context.Context) (*model.SpotifyPlaying, error)
|
||||
SpotifyRecent(ctx context.Context) ([]*model.SpotifyRecentItem, error)
|
||||
GiteaFeed(ctx context.Context) (*model.GiteaFeedItem, error)
|
||||
Me(ctx context.Context) (*models.User, error)
|
||||
}
|
||||
type RowingResolver interface {
|
||||
@@ -304,6 +315,43 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
||||
|
||||
return e.ComplexityRoot.Favorite.UpdatedAt(childComplexity), true
|
||||
|
||||
case "GiteaFeedItem.avatarUrl":
|
||||
if e.ComplexityRoot.GiteaFeedItem.AvatarURL == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.ComplexityRoot.GiteaFeedItem.AvatarURL(childComplexity), true
|
||||
case "GiteaFeedItem.commitMessage":
|
||||
if e.ComplexityRoot.GiteaFeedItem.CommitMessage == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.ComplexityRoot.GiteaFeedItem.CommitMessage(childComplexity), true
|
||||
case "GiteaFeedItem.createdAt":
|
||||
if e.ComplexityRoot.GiteaFeedItem.CreatedAt == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.ComplexityRoot.GiteaFeedItem.CreatedAt(childComplexity), true
|
||||
case "GiteaFeedItem.opType":
|
||||
if e.ComplexityRoot.GiteaFeedItem.OpType == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.ComplexityRoot.GiteaFeedItem.OpType(childComplexity), true
|
||||
case "GiteaFeedItem.repoName":
|
||||
if e.ComplexityRoot.GiteaFeedItem.RepoName == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.ComplexityRoot.GiteaFeedItem.RepoName(childComplexity), true
|
||||
case "GiteaFeedItem.repoUrl":
|
||||
if e.ComplexityRoot.GiteaFeedItem.RepoURL == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.ComplexityRoot.GiteaFeedItem.RepoURL(childComplexity), true
|
||||
|
||||
case "Message.authorId":
|
||||
if e.ComplexityRoot.Message.AuthorID == nil {
|
||||
break
|
||||
@@ -496,6 +544,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
||||
}
|
||||
|
||||
return e.ComplexityRoot.Query.Favorites(childComplexity), true
|
||||
case "Query.giteaFeed":
|
||||
if e.ComplexityRoot.Query.GiteaFeed == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.ComplexityRoot.Query.GiteaFeed(childComplexity), true
|
||||
|
||||
case "Query.me":
|
||||
if e.ComplexityRoot.Query.Me == nil {
|
||||
@@ -796,7 +850,7 @@ func newExecutionContext(
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed "schema/activity.graphql" "schema/auth.graphql" "schema/favorite.graphql" "schema/message.graphql" "schema/post.graphql" "schema/rowing.graphql" "schema/schema.graphql" "schema/spotify.graphql" "schema/user.graphql"
|
||||
//go:embed "schema/activity.graphql" "schema/auth.graphql" "schema/favorite.graphql" "schema/gitea.graphql" "schema/message.graphql" "schema/post.graphql" "schema/rowing.graphql" "schema/schema.graphql" "schema/spotify.graphql" "schema/user.graphql"
|
||||
var sourcesFS embed.FS
|
||||
|
||||
func sourceData(filename string) string {
|
||||
@@ -811,6 +865,7 @@ var sources = []*ast.Source{
|
||||
{Name: "schema/activity.graphql", Input: sourceData("schema/activity.graphql"), BuiltIn: false},
|
||||
{Name: "schema/auth.graphql", Input: sourceData("schema/auth.graphql"), BuiltIn: false},
|
||||
{Name: "schema/favorite.graphql", Input: sourceData("schema/favorite.graphql"), BuiltIn: false},
|
||||
{Name: "schema/gitea.graphql", Input: sourceData("schema/gitea.graphql"), BuiltIn: false},
|
||||
{Name: "schema/message.graphql", Input: sourceData("schema/message.graphql"), BuiltIn: false},
|
||||
{Name: "schema/post.graphql", Input: sourceData("schema/post.graphql"), BuiltIn: false},
|
||||
{Name: "schema/rowing.graphql", Input: sourceData("schema/rowing.graphql"), BuiltIn: false},
|
||||
@@ -1407,6 +1462,180 @@ func (ec *executionContext) fieldContext_Favorite_link(_ context.Context, field
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _GiteaFeedItem_avatarUrl(ctx context.Context, field graphql.CollectedField, obj *model.GiteaFeedItem) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
ec.OperationContext,
|
||||
field,
|
||||
ec.fieldContext_GiteaFeedItem_avatarUrl,
|
||||
func(ctx context.Context) (any, error) {
|
||||
return obj.AvatarURL, nil
|
||||
},
|
||||
nil,
|
||||
ec.marshalNString2string,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_GiteaFeedItem_avatarUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "GiteaFeedItem",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type String does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _GiteaFeedItem_repoUrl(ctx context.Context, field graphql.CollectedField, obj *model.GiteaFeedItem) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
ec.OperationContext,
|
||||
field,
|
||||
ec.fieldContext_GiteaFeedItem_repoUrl,
|
||||
func(ctx context.Context) (any, error) {
|
||||
return obj.RepoURL, nil
|
||||
},
|
||||
nil,
|
||||
ec.marshalNString2string,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_GiteaFeedItem_repoUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "GiteaFeedItem",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type String does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _GiteaFeedItem_repoName(ctx context.Context, field graphql.CollectedField, obj *model.GiteaFeedItem) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
ec.OperationContext,
|
||||
field,
|
||||
ec.fieldContext_GiteaFeedItem_repoName,
|
||||
func(ctx context.Context) (any, error) {
|
||||
return obj.RepoName, nil
|
||||
},
|
||||
nil,
|
||||
ec.marshalNString2string,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_GiteaFeedItem_repoName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "GiteaFeedItem",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type String does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _GiteaFeedItem_opType(ctx context.Context, field graphql.CollectedField, obj *model.GiteaFeedItem) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
ec.OperationContext,
|
||||
field,
|
||||
ec.fieldContext_GiteaFeedItem_opType,
|
||||
func(ctx context.Context) (any, error) {
|
||||
return obj.OpType, nil
|
||||
},
|
||||
nil,
|
||||
ec.marshalNString2string,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_GiteaFeedItem_opType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "GiteaFeedItem",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type String does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _GiteaFeedItem_commitMessage(ctx context.Context, field graphql.CollectedField, obj *model.GiteaFeedItem) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
ec.OperationContext,
|
||||
field,
|
||||
ec.fieldContext_GiteaFeedItem_commitMessage,
|
||||
func(ctx context.Context) (any, error) {
|
||||
return obj.CommitMessage, nil
|
||||
},
|
||||
nil,
|
||||
ec.marshalNString2string,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_GiteaFeedItem_commitMessage(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "GiteaFeedItem",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type String does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _GiteaFeedItem_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.GiteaFeedItem) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
ec.OperationContext,
|
||||
field,
|
||||
ec.fieldContext_GiteaFeedItem_createdAt,
|
||||
func(ctx context.Context) (any, error) {
|
||||
return obj.CreatedAt, nil
|
||||
},
|
||||
nil,
|
||||
ec.marshalNTime2timeᚐTime,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_GiteaFeedItem_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "GiteaFeedItem",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type Time does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Message_id(ctx context.Context, field graphql.CollectedField, obj *models.Message) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
@@ -2713,6 +2942,49 @@ func (ec *executionContext) fieldContext_Query_spotifyRecent(_ context.Context,
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Query_giteaFeed(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
ec.OperationContext,
|
||||
field,
|
||||
ec.fieldContext_Query_giteaFeed,
|
||||
func(ctx context.Context) (any, error) {
|
||||
return ec.Resolvers.Query().GiteaFeed(ctx)
|
||||
},
|
||||
nil,
|
||||
ec.marshalOGiteaFeedItem2ᚖadamᚑfrenchᚗcoᚗukᚋbackendᚋgraphᚋmodelᚐGiteaFeedItem,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_Query_giteaFeed(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "Query",
|
||||
Field: field,
|
||||
IsMethod: true,
|
||||
IsResolver: true,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
switch field.Name {
|
||||
case "avatarUrl":
|
||||
return ec.fieldContext_GiteaFeedItem_avatarUrl(ctx, field)
|
||||
case "repoUrl":
|
||||
return ec.fieldContext_GiteaFeedItem_repoUrl(ctx, field)
|
||||
case "repoName":
|
||||
return ec.fieldContext_GiteaFeedItem_repoName(ctx, field)
|
||||
case "opType":
|
||||
return ec.fieldContext_GiteaFeedItem_opType(ctx, field)
|
||||
case "commitMessage":
|
||||
return ec.fieldContext_GiteaFeedItem_commitMessage(ctx, field)
|
||||
case "createdAt":
|
||||
return ec.fieldContext_GiteaFeedItem_createdAt(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type GiteaFeedItem", field.Name)
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Query_me(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
@@ -5472,6 +5744,70 @@ func (ec *executionContext) _Favorite(ctx context.Context, sel ast.SelectionSet,
|
||||
return out
|
||||
}
|
||||
|
||||
var giteaFeedItemImplementors = []string{"GiteaFeedItem"}
|
||||
|
||||
func (ec *executionContext) _GiteaFeedItem(ctx context.Context, sel ast.SelectionSet, obj *model.GiteaFeedItem) graphql.Marshaler {
|
||||
fields := graphql.CollectFields(ec.OperationContext, sel, giteaFeedItemImplementors)
|
||||
|
||||
out := graphql.NewFieldSet(fields)
|
||||
deferred := make(map[string]*graphql.FieldSet)
|
||||
for i, field := range fields {
|
||||
switch field.Name {
|
||||
case "__typename":
|
||||
out.Values[i] = graphql.MarshalString("GiteaFeedItem")
|
||||
case "avatarUrl":
|
||||
out.Values[i] = ec._GiteaFeedItem_avatarUrl(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
case "repoUrl":
|
||||
out.Values[i] = ec._GiteaFeedItem_repoUrl(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
case "repoName":
|
||||
out.Values[i] = ec._GiteaFeedItem_repoName(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
case "opType":
|
||||
out.Values[i] = ec._GiteaFeedItem_opType(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
case "commitMessage":
|
||||
out.Values[i] = ec._GiteaFeedItem_commitMessage(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
case "createdAt":
|
||||
out.Values[i] = ec._GiteaFeedItem_createdAt(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
}
|
||||
out.Dispatch(ctx)
|
||||
if out.Invalids > 0 {
|
||||
return graphql.Null
|
||||
}
|
||||
|
||||
atomic.AddInt32(&ec.Deferred, int32(len(deferred)))
|
||||
|
||||
for label, dfs := range deferred {
|
||||
ec.ProcessDeferredGroup(graphql.DeferredGroup{
|
||||
Label: label,
|
||||
Path: graphql.GetPath(ctx),
|
||||
FieldSet: dfs,
|
||||
Context: ctx,
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
var messageImplementors = []string{"Message"}
|
||||
|
||||
func (ec *executionContext) _Message(ctx context.Context, sel ast.SelectionSet, obj *models.Message) graphql.Marshaler {
|
||||
@@ -6030,6 +6366,25 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
|
||||
func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
|
||||
}
|
||||
|
||||
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
|
||||
case "giteaFeed":
|
||||
field := field
|
||||
|
||||
innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
}
|
||||
}()
|
||||
res = ec._Query_giteaFeed(ctx, field)
|
||||
return res
|
||||
}
|
||||
|
||||
rrm := func(ctx context.Context) graphql.Marshaler {
|
||||
return ec.OperationContext.RootResolverMiddleware(ctx,
|
||||
func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
|
||||
}
|
||||
|
||||
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
|
||||
case "me":
|
||||
field := field
|
||||
@@ -7505,6 +7860,13 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast
|
||||
return res
|
||||
}
|
||||
|
||||
func (ec *executionContext) marshalOGiteaFeedItem2ᚖadamᚑfrenchᚗcoᚗukᚋbackendᚋgraphᚋmodelᚐGiteaFeedItem(ctx context.Context, sel ast.SelectionSet, v *model.GiteaFeedItem) graphql.Marshaler {
|
||||
if v == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
return ec._GiteaFeedItem(ctx, sel, v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) marshalOPost2ᚖadamᚑfrenchᚗcoᚗukᚋbackendᚋmodelsᚐPost(ctx context.Context, sel ast.SelectionSet, v *models.Post) graphql.Marshaler {
|
||||
if v == nil {
|
||||
return graphql.Null
|
||||
|
||||
17
backend/graph/gitea_helpers.go
Normal file
17
backend/graph/gitea_helpers.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"adam-french.co.uk/backend/graph/model"
|
||||
"adam-french.co.uk/backend/services"
|
||||
)
|
||||
|
||||
func mapGiteaFeed(feed *services.GiteaFeedResponse) *model.GiteaFeedItem {
|
||||
return &model.GiteaFeedItem{
|
||||
AvatarURL: feed.ActUser.AvatarURL,
|
||||
RepoURL: feed.Repo.HTMLURL,
|
||||
RepoName: feed.Repo.FullName,
|
||||
OpType: feed.OpType,
|
||||
CommitMessage: services.ParseCommitMessage(feed.Content),
|
||||
CreatedAt: feed.Created,
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,15 @@ type CreateUserInput struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type GiteaFeedItem struct {
|
||||
AvatarURL string `json:"avatarUrl"`
|
||||
RepoURL string `json:"repoUrl"`
|
||||
RepoName string `json:"repoName"`
|
||||
OpType string `json:"opType"`
|
||||
CommitMessage string `json:"commitMessage"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type LoginInput struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"adam-french.co.uk/backend/graph/model"
|
||||
"adam-french.co.uk/backend/models"
|
||||
"adam-french.co.uk/backend/services"
|
||||
spotify "github.com/zmb3/spotify/v2"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -429,6 +430,26 @@ func (r *queryResolver) SpotifyRecent(ctx context.Context) ([]*model.SpotifyRece
|
||||
return mapRecentItems(played), nil
|
||||
}
|
||||
|
||||
// GiteaFeed is the resolver for the giteaFeed field.
|
||||
func (r *queryResolver) GiteaFeed(ctx context.Context) (*model.GiteaFeedItem, error) {
|
||||
if r.Store.GiteaFeedFresh() {
|
||||
return mapGiteaFeed(r.Store.GiteaFeed), nil
|
||||
}
|
||||
|
||||
feed, err := services.FetchLatestFeed(r.Store.GiteaHost, r.Store.GiteaPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if feed == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
r.Store.GiteaFeed = feed
|
||||
r.Store.GiteaFeedFetchedAt = time.Now()
|
||||
|
||||
return mapGiteaFeed(feed), nil
|
||||
}
|
||||
|
||||
// Me is the resolver for the me field.
|
||||
func (r *queryResolver) Me(ctx context.Context) (*models.User, error) {
|
||||
userID, ok := UserIDFromCtx(ctx)
|
||||
@@ -453,4 +474,3 @@ func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
|
||||
|
||||
type mutationResolver struct{ *Resolver }
|
||||
type queryResolver struct{ *Resolver }
|
||||
|
||||
|
||||
8
backend/graph/schema/gitea.graphql
Normal file
8
backend/graph/schema/gitea.graphql
Normal file
@@ -0,0 +1,8 @@
|
||||
type GiteaFeedItem {
|
||||
avatarUrl: String!
|
||||
repoUrl: String!
|
||||
repoName: String!
|
||||
opType: String!
|
||||
commitMessage: String!
|
||||
createdAt: Time!
|
||||
}
|
||||
@@ -11,6 +11,7 @@ type Query {
|
||||
messages: [Message!]!
|
||||
spotifyListening: SpotifyPlaying
|
||||
spotifyRecent: [SpotifyRecentItem!]!
|
||||
giteaFeed: GiteaFeedItem
|
||||
me: User
|
||||
}
|
||||
|
||||
|
||||
@@ -20,4 +20,16 @@ type Store struct {
|
||||
|
||||
RecentSongs *[]spotify.RecentlyPlayedItem
|
||||
RecentSongsFetchedAt time.Time
|
||||
|
||||
GiteaHost string
|
||||
GiteaPort string
|
||||
GiteaFeed *services.GiteaFeedResponse
|
||||
GiteaFeedFetchedAt time.Time
|
||||
}
|
||||
|
||||
func (s *Store) GiteaFeedFresh() bool {
|
||||
if s.GiteaFeed == nil {
|
||||
return false
|
||||
}
|
||||
return time.Since(s.GiteaFeedFetchedAt) < time.Minute
|
||||
}
|
||||
|
||||
@@ -71,7 +71,10 @@ func main() {
|
||||
notesConfig := services.NotesConfig{Dir: notesDir}
|
||||
notes := services.InitNotes(¬esConfig)
|
||||
|
||||
store := handlers.Store{DB: db, SpotifyAuth: spotifyAuth, SpotifyClient: spotifyClient, ClaudeClient: claudeClient, Auth: auth, Notes: notes}
|
||||
giteaHost := os.Getenv("GITEA_HOST")
|
||||
giteaPort := os.Getenv("GITEA_PORT")
|
||||
|
||||
store := handlers.Store{DB: db, SpotifyAuth: spotifyAuth, SpotifyClient: spotifyClient, ClaudeClient: claudeClient, Auth: auth, Notes: notes, GiteaHost: giteaHost, GiteaPort: giteaPort}
|
||||
|
||||
protected := r.Group("/", store.AuthMiddlewear)
|
||||
admin := r.Group("/", store.AuthMiddlewear, store.AdminMiddleware)
|
||||
|
||||
72
backend/services/gitea.go
Normal file
72
backend/services/gitea.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GiteaFeedCommit struct {
|
||||
Message string `json:"Message"`
|
||||
}
|
||||
|
||||
type GiteaFeedContent struct {
|
||||
Commits []GiteaFeedCommit `json:"Commits"`
|
||||
}
|
||||
|
||||
type GiteaFeedUser struct {
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
}
|
||||
|
||||
type GiteaFeedRepo struct {
|
||||
FullName string `json:"full_name"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
}
|
||||
|
||||
type GiteaFeedResponse struct {
|
||||
ActUser GiteaFeedUser `json:"act_user"`
|
||||
Repo GiteaFeedRepo `json:"repo"`
|
||||
OpType string `json:"op_type"`
|
||||
Content string `json:"content"`
|
||||
Created time.Time `json:"created"`
|
||||
}
|
||||
|
||||
func FetchLatestFeed(host, port string) (*GiteaFeedResponse, error) {
|
||||
url := fmt.Sprintf("http://%s:%s/api/v1/users/adamf/activities/feeds?limit=1", host, port)
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var items []GiteaFeedResponse
|
||||
if err := json.Unmarshal(body, &items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &items[0], nil
|
||||
}
|
||||
|
||||
func ParseCommitMessage(content string) string {
|
||||
var c GiteaFeedContent
|
||||
if err := json.Unmarshal([]byte(content), &c); err != nil {
|
||||
return ""
|
||||
}
|
||||
if len(c.Commits) == 0 {
|
||||
return ""
|
||||
}
|
||||
return c.Commits[0].Message
|
||||
}
|
||||
Reference in New Issue
Block a user