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
|
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 {
|
Message struct {
|
||||||
AuthorID func(childComplexity int) int
|
AuthorID func(childComplexity int) int
|
||||||
Content func(childComplexity int) int
|
Content func(childComplexity int) int
|
||||||
@@ -100,6 +109,7 @@ type ComplexityRoot struct {
|
|||||||
Query struct {
|
Query struct {
|
||||||
Activities func(childComplexity int) int
|
Activities func(childComplexity int) int
|
||||||
Favorites func(childComplexity int) int
|
Favorites func(childComplexity int) int
|
||||||
|
GiteaFeed func(childComplexity int) int
|
||||||
Me func(childComplexity int) int
|
Me func(childComplexity int) int
|
||||||
Messages func(childComplexity int) int
|
Messages func(childComplexity int) int
|
||||||
Post func(childComplexity int, id int) int
|
Post func(childComplexity int, id int) int
|
||||||
@@ -197,6 +207,7 @@ type QueryResolver interface {
|
|||||||
Messages(ctx context.Context) ([]*models.Message, error)
|
Messages(ctx context.Context) ([]*models.Message, error)
|
||||||
SpotifyListening(ctx context.Context) (*model.SpotifyPlaying, error)
|
SpotifyListening(ctx context.Context) (*model.SpotifyPlaying, error)
|
||||||
SpotifyRecent(ctx context.Context) ([]*model.SpotifyRecentItem, error)
|
SpotifyRecent(ctx context.Context) ([]*model.SpotifyRecentItem, error)
|
||||||
|
GiteaFeed(ctx context.Context) (*model.GiteaFeedItem, error)
|
||||||
Me(ctx context.Context) (*models.User, error)
|
Me(ctx context.Context) (*models.User, error)
|
||||||
}
|
}
|
||||||
type RowingResolver interface {
|
type RowingResolver interface {
|
||||||
@@ -304,6 +315,43 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
|
|
||||||
return e.ComplexityRoot.Favorite.UpdatedAt(childComplexity), true
|
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":
|
case "Message.authorId":
|
||||||
if e.ComplexityRoot.Message.AuthorID == nil {
|
if e.ComplexityRoot.Message.AuthorID == nil {
|
||||||
break
|
break
|
||||||
@@ -496,6 +544,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
return e.ComplexityRoot.Query.Favorites(childComplexity), true
|
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":
|
case "Query.me":
|
||||||
if e.ComplexityRoot.Query.Me == nil {
|
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
|
var sourcesFS embed.FS
|
||||||
|
|
||||||
func sourceData(filename string) string {
|
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/activity.graphql", Input: sourceData("schema/activity.graphql"), BuiltIn: false},
|
||||||
{Name: "schema/auth.graphql", Input: sourceData("schema/auth.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/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/message.graphql", Input: sourceData("schema/message.graphql"), BuiltIn: false},
|
||||||
{Name: "schema/post.graphql", Input: sourceData("schema/post.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},
|
{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
|
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) {
|
func (ec *executionContext) _Message_id(ctx context.Context, field graphql.CollectedField, obj *models.Message) (ret graphql.Marshaler) {
|
||||||
return graphql.ResolveField(
|
return graphql.ResolveField(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -2713,6 +2942,49 @@ func (ec *executionContext) fieldContext_Query_spotifyRecent(_ context.Context,
|
|||||||
return fc, nil
|
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) {
|
func (ec *executionContext) _Query_me(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
return graphql.ResolveField(
|
return graphql.ResolveField(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -5472,6 +5744,70 @@ func (ec *executionContext) _Favorite(ctx context.Context, sel ast.SelectionSet,
|
|||||||
return out
|
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"}
|
var messageImplementors = []string{"Message"}
|
||||||
|
|
||||||
func (ec *executionContext) _Message(ctx context.Context, sel ast.SelectionSet, obj *models.Message) graphql.Marshaler {
|
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) })
|
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) })
|
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
|
||||||
case "me":
|
case "me":
|
||||||
field := field
|
field := field
|
||||||
@@ -7505,6 +7860,13 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast
|
|||||||
return res
|
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 {
|
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 {
|
if v == nil {
|
||||||
return graphql.Null
|
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"`
|
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 {
|
type LoginInput struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"adam-french.co.uk/backend/graph/model"
|
"adam-french.co.uk/backend/graph/model"
|
||||||
"adam-french.co.uk/backend/models"
|
"adam-french.co.uk/backend/models"
|
||||||
|
"adam-french.co.uk/backend/services"
|
||||||
spotify "github.com/zmb3/spotify/v2"
|
spotify "github.com/zmb3/spotify/v2"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
@@ -429,6 +430,26 @@ func (r *queryResolver) SpotifyRecent(ctx context.Context) ([]*model.SpotifyRece
|
|||||||
return mapRecentItems(played), nil
|
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.
|
// Me is the resolver for the me field.
|
||||||
func (r *queryResolver) Me(ctx context.Context) (*models.User, error) {
|
func (r *queryResolver) Me(ctx context.Context) (*models.User, error) {
|
||||||
userID, ok := UserIDFromCtx(ctx)
|
userID, ok := UserIDFromCtx(ctx)
|
||||||
@@ -453,4 +474,3 @@ func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
|
|||||||
|
|
||||||
type mutationResolver struct{ *Resolver }
|
type mutationResolver struct{ *Resolver }
|
||||||
type queryResolver 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!]!
|
messages: [Message!]!
|
||||||
spotifyListening: SpotifyPlaying
|
spotifyListening: SpotifyPlaying
|
||||||
spotifyRecent: [SpotifyRecentItem!]!
|
spotifyRecent: [SpotifyRecentItem!]!
|
||||||
|
giteaFeed: GiteaFeedItem
|
||||||
me: User
|
me: User
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,4 +20,16 @@ type Store struct {
|
|||||||
|
|
||||||
RecentSongs *[]spotify.RecentlyPlayedItem
|
RecentSongs *[]spotify.RecentlyPlayedItem
|
||||||
RecentSongsFetchedAt time.Time
|
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}
|
notesConfig := services.NotesConfig{Dir: notesDir}
|
||||||
notes := services.InitNotes(¬esConfig)
|
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)
|
protected := r.Group("/", store.AuthMiddlewear)
|
||||||
admin := r.Group("/", store.AuthMiddlewear, store.AdminMiddleware)
|
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
|
||||||
|
}
|
||||||
@@ -1,59 +1,36 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import axios from "axios";
|
import { useHomeDataStore } from "@/stores/homeData";
|
||||||
import { ref, onMounted } from "vue";
|
import { storeToRefs } from "pinia";
|
||||||
import Header from "@/components/text/Header.vue";
|
import Header from "@/components/text/Header.vue";
|
||||||
|
|
||||||
const url = "/gitea/api/v1/users/adamf/activities/feeds?limit=1";
|
const homeData = useHomeDataStore();
|
||||||
|
const { gitFeed: feed, loaded } = storeToRefs(homeData);
|
||||||
const feed = ref(null);
|
|
||||||
const isLoading = ref(true);
|
|
||||||
const hasError = ref(false);
|
|
||||||
|
|
||||||
async function checkFeed() {
|
|
||||||
try {
|
|
||||||
const res = await axios.get(url);
|
|
||||||
feed.value = res.data[0] || null;
|
|
||||||
hasError.value = false;
|
|
||||||
} catch (err) {
|
|
||||||
hasError.value = true;
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
checkFeed();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col text-center h-full">
|
<div class="flex flex-col text-center h-full">
|
||||||
<Header class="text-left">Commits</Header>
|
<Header class="text-left">Commits</Header>
|
||||||
|
|
||||||
<div v-if="isLoading" class="flex-1 overflow-y-auto">
|
<div v-if="!loaded" class="flex-1 overflow-y-auto">
|
||||||
<p>Loading latest activity...</p>
|
<p>Loading latest activity...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="hasError" class="flex-1 overflow-y-auto">
|
|
||||||
<p>Could not fetch feed. Please try again later.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else-if="feed"
|
v-else-if="feed"
|
||||||
class="flex-1 flex flex-col justify-center overflow-y-auto"
|
class="flex-1 flex flex-col justify-center overflow-y-auto"
|
||||||
>
|
>
|
||||||
<h3>Last git activity</h3>
|
<h3>Last git activity</h3>
|
||||||
<img
|
<img
|
||||||
:src="feed.act_user.avatar_url"
|
:src="feed.avatarUrl"
|
||||||
alt="User avatar"
|
alt="User avatar"
|
||||||
class="avatar"
|
class="avatar"
|
||||||
/>
|
/>
|
||||||
<a :href="feed.repo.html_url">
|
<a :href="feed.repoUrl">
|
||||||
<h3>repo: {{ feed.repo.full_name }}</h3>
|
<h3>repo: {{ feed.repoName }}</h3>
|
||||||
</a>
|
</a>
|
||||||
<p>Action: {{ feed.op_type }}</p>
|
<p>Action: {{ feed.opType }}</p>
|
||||||
<p>Message: {{ JSON.parse(feed.content).Commits[0].Message }}</p>
|
<p>Message: {{ feed.commitMessage }}</p>
|
||||||
<small> {{ new Date(feed.created).toLocaleString() }}</small>
|
<small> {{ new Date(feed.createdAt).toLocaleString() }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="flex-1 overflow-y-auto">
|
<div v-else class="flex-1 overflow-y-auto">
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ export const useHomeDataStore = defineStore("homeData", () => {
|
|||||||
activities { id type name link createdAt }
|
activities { id type name link createdAt }
|
||||||
spotifyRecent { track { name album { name images { url } } artists { name } } playedAt }
|
spotifyRecent { track { name album { name images { url } } artists { name } } playedAt }
|
||||||
rowingSessions { id date time distance timePer500m calories }
|
rowingSessions { id date time distance timePer500m calories }
|
||||||
|
giteaFeed { avatarUrl repoUrl repoName opType commitMessage createdAt }
|
||||||
me { id username admin }
|
me { id username admin }
|
||||||
}
|
}
|
||||||
`),
|
`),
|
||||||
fetchGitFeed(),
|
|
||||||
fetchRadioStatus(),
|
fetchRadioStatus(),
|
||||||
]);
|
]);
|
||||||
posts.value = data.posts;
|
posts.value = data.posts;
|
||||||
@@ -37,6 +37,7 @@ export const useHomeDataStore = defineStore("homeData", () => {
|
|||||||
activities.value = data.activities;
|
activities.value = data.activities;
|
||||||
spotifyRecent.value = data.spotifyRecent;
|
spotifyRecent.value = data.spotifyRecent;
|
||||||
rowingSessions.value = data.rowingSessions;
|
rowingSessions.value = data.rowingSessions;
|
||||||
|
gitFeed.value = data.giteaFeed || null;
|
||||||
me.value = data.me || null;
|
me.value = data.me || null;
|
||||||
loaded.value = true;
|
loaded.value = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -45,15 +46,6 @@ export const useHomeDataStore = defineStore("homeData", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchGitFeed() {
|
|
||||||
try {
|
|
||||||
const res = await axios.get("/gitea/api/v1/users/adamf/activities/feeds?limit=1");
|
|
||||||
gitFeed.value = res.data[0] || null;
|
|
||||||
} catch {
|
|
||||||
gitFeed.value = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchRadioStatus() {
|
async function fetchRadioStatus() {
|
||||||
try {
|
try {
|
||||||
await axios.head("/radio/stream");
|
await axios.head("/radio/stream");
|
||||||
|
|||||||
Reference in New Issue
Block a user