v0.0.27 Изменен принцип загрузки изображения в s3
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
admin 2025-08-28 09:55:17 +03:00
parent a9865fdc3c
commit ef9bb2c484
7 changed files with 603 additions and 22 deletions

View File

@ -140,7 +140,7 @@ type ComplexityRoot struct {
ConfirmEmail func(childComplexity int, token string) int ConfirmEmail func(childComplexity int, token string) int
CreateChat func(childComplexity int, user1Id int, user2Id int) int CreateChat func(childComplexity int, user1Id int, user2Id int) int
CreateComment func(childComplexity int, postID int, content string) int CreateComment func(childComplexity int, postID int, content string) int
CreatePost func(childComplexity int, title string, content string) int CreatePost func(childComplexity int, input CreatePostInput) int
DeletePost func(childComplexity int, id int) int DeletePost func(childComplexity int, id int) int
FollowUser func(childComplexity int, followingID int) int FollowUser func(childComplexity int, followingID int) int
LikePost func(childComplexity int, postID int) int LikePost func(childComplexity int, postID int) int
@ -180,6 +180,7 @@ type ComplexityRoot struct {
Query struct { Query struct {
Comments func(childComplexity int, postID int) int Comments func(childComplexity int, postID int) int
GenerateUploadURL func(childComplexity int, filename string) int
GetChat func(childComplexity int, user1Id int, user2Id int) int GetChat func(childComplexity int, user1Id int, user2Id int) int
GetChatMessages func(childComplexity int, chatID int, limit int, offset int) int GetChatMessages func(childComplexity int, chatID int, limit int, offset int) int
GetFollowers func(childComplexity int, userID int, limit *int, offset *int) int GetFollowers func(childComplexity int, userID int, limit *int, offset *int) int
@ -234,6 +235,12 @@ type ComplexityRoot struct {
Success func(childComplexity int) int Success func(childComplexity int) int
} }
UploadURL struct {
ExpiresAt func(childComplexity int) int
Key func(childComplexity int) int
URL func(childComplexity int) int
}
User struct { User struct {
Avatar func(childComplexity int) int Avatar func(childComplexity int) int
CreatedAt func(childComplexity int) int CreatedAt func(childComplexity int) int
@ -277,7 +284,7 @@ type MutationResolver interface {
Register(ctx context.Context, input domain.RegisterInput) (*domain.User, error) Register(ctx context.Context, input domain.RegisterInput) (*domain.User, error)
Login(ctx context.Context, input domain.LoginInput) (*domain.Tokens, error) Login(ctx context.Context, input domain.LoginInput) (*domain.Tokens, error)
RefreshTokens(ctx context.Context, refreshToken string) (*domain.Tokens, error) RefreshTokens(ctx context.Context, refreshToken string) (*domain.Tokens, error)
CreatePost(ctx context.Context, title string, content string) (*domain.Post, error) CreatePost(ctx context.Context, input CreatePostInput) (*domain.Post, error)
CreateComment(ctx context.Context, postID int, content string) (*domain.Comment, error) CreateComment(ctx context.Context, postID int, content string) (*domain.Comment, error)
LikePost(ctx context.Context, postID int) (*domain.Like, error) LikePost(ctx context.Context, postID int) (*domain.Like, error)
UnlikePost(ctx context.Context, postID int) (bool, error) UnlikePost(ctx context.Context, postID int) (bool, error)
@ -310,6 +317,7 @@ type QueryResolver interface {
Post(ctx context.Context, id int) (*domain.Post, error) Post(ctx context.Context, id int) (*domain.Post, error)
Posts(ctx context.Context) ([]*domain.Post, error) Posts(ctx context.Context) ([]*domain.Post, error)
PostsPaginated(ctx context.Context, limit int, offset int) ([]*domain.Post, error) PostsPaginated(ctx context.Context, limit int, offset int) ([]*domain.Post, error)
GenerateUploadURL(ctx context.Context, filename string) (*UploadURL, error)
GetUserPosts(ctx context.Context, userID int) ([]*domain.Post, error) GetUserPosts(ctx context.Context, userID int) ([]*domain.Post, error)
User(ctx context.Context, id int) (*domain.User, error) User(ctx context.Context, id int) (*domain.User, error)
Users(ctx context.Context) ([]*domain.User, error) Users(ctx context.Context) ([]*domain.User, error)
@ -733,7 +741,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return 0, false return 0, false
} }
return e.complexity.Mutation.CreatePost(childComplexity, args["title"].(string), args["content"].(string)), true return e.complexity.Mutation.CreatePost(childComplexity, args["input"].(CreatePostInput)), true
case "Mutation.deletePost": case "Mutation.deletePost":
if e.complexity.Mutation.DeletePost == nil { if e.complexity.Mutation.DeletePost == nil {
@ -1020,6 +1028,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Query.Comments(childComplexity, args["postID"].(int)), true return e.complexity.Query.Comments(childComplexity, args["postID"].(int)), true
case "Query.generateUploadURL":
if e.complexity.Query.GenerateUploadURL == nil {
break
}
args, err := ec.field_Query_generateUploadURL_args(ctx, rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Query.GenerateUploadURL(childComplexity, args["filename"].(string)), true
case "Query.getChat": case "Query.getChat":
if e.complexity.Query.GetChat == nil { if e.complexity.Query.GetChat == nil {
break break
@ -1349,6 +1369,27 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.UnfollowResult.Success(childComplexity), true return e.complexity.UnfollowResult.Success(childComplexity), true
case "UploadURL.expiresAt":
if e.complexity.UploadURL.ExpiresAt == nil {
break
}
return e.complexity.UploadURL.ExpiresAt(childComplexity), true
case "UploadURL.key":
if e.complexity.UploadURL.Key == nil {
break
}
return e.complexity.UploadURL.Key(childComplexity), true
case "UploadURL.url":
if e.complexity.UploadURL.URL == nil {
break
}
return e.complexity.UploadURL.URL(childComplexity), true
case "User.avatar": case "User.avatar":
if e.complexity.User.Avatar == nil { if e.complexity.User.Avatar == nil {
break break
@ -1468,6 +1509,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
opCtx := graphql.GetOperationContext(ctx) opCtx := graphql.GetOperationContext(ctx)
ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)} ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)}
inputUnmarshalMap := graphql.BuildUnmarshalerMap( inputUnmarshalMap := graphql.BuildUnmarshalerMap(
ec.unmarshalInputCreatePostInput,
ec.unmarshalInputLoginInput, ec.unmarshalInputLoginInput,
ec.unmarshalInputRegisterInput, ec.unmarshalInputRegisterInput,
) )
@ -1685,16 +1727,11 @@ func (ec *executionContext) field_Mutation_createComment_args(ctx context.Contex
func (ec *executionContext) field_Mutation_createPost_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { func (ec *executionContext) field_Mutation_createPost_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error var err error
args := map[string]any{} args := map[string]any{}
arg0, err := processArgField(ctx, rawArgs, "title", ec.unmarshalNString2string) arg0, err := processArgField(ctx, rawArgs, "input", ec.unmarshalNCreatePostInput2tailly_back_v2ᚋinternalᚋhttpᚋgraphᚐCreatePostInput)
if err != nil { if err != nil {
return nil, err return nil, err
} }
args["title"] = arg0 args["input"] = arg0
arg1, err := processArgField(ctx, rawArgs, "content", ec.unmarshalNString2string)
if err != nil {
return nil, err
}
args["content"] = arg1
return args, nil return args, nil
} }
@ -1899,6 +1936,17 @@ func (ec *executionContext) field_Query_comments_args(ctx context.Context, rawAr
return args, nil return args, nil
} }
func (ec *executionContext) field_Query_generateUploadURL_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := processArgField(ctx, rawArgs, "filename", ec.unmarshalNString2string)
if err != nil {
return nil, err
}
args["filename"] = arg0
return args, nil
}
func (ec *executionContext) field_Query_getChatMessages_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { func (ec *executionContext) field_Query_getChatMessages_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error var err error
args := map[string]any{} args := map[string]any{}
@ -4519,7 +4567,7 @@ func (ec *executionContext) _Mutation_createPost(ctx context.Context, field grap
}() }()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().CreatePost(rctx, fc.Args["title"].(string), fc.Args["content"].(string)) return ec.resolvers.Mutation().CreatePost(rctx, fc.Args["input"].(CreatePostInput))
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@ -6541,6 +6589,69 @@ func (ec *executionContext) fieldContext_Query_postsPaginated(ctx context.Contex
return fc, nil return fc, nil
} }
func (ec *executionContext) _Query_generateUploadURL(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query_generateUploadURL(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().GenerateUploadURL(rctx, fc.Args["filename"].(string))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*UploadURL)
fc.Result = res
return ec.marshalNUploadURL2ᚖtailly_back_v2ᚋinternalᚋhttpᚋgraphᚐUploadURL(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Query_generateUploadURL(ctx 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 "url":
return ec.fieldContext_UploadURL_url(ctx, field)
case "key":
return ec.fieldContext_UploadURL_key(ctx, field)
case "expiresAt":
return ec.fieldContext_UploadURL_expiresAt(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type UploadURL", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Query_generateUploadURL_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}
func (ec *executionContext) _Query_getUserPosts(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Query_getUserPosts(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query_getUserPosts(ctx, field) fc, err := ec.fieldContext_Query_getUserPosts(ctx, field)
if err != nil { if err != nil {
@ -8521,6 +8632,138 @@ func (ec *executionContext) fieldContext_UnfollowResult_message(_ context.Contex
return fc, nil return fc, nil
} }
func (ec *executionContext) _UploadURL_url(ctx context.Context, field graphql.CollectedField, obj *UploadURL) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_UploadURL_url(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.URL, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_UploadURL_url(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "UploadURL",
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) _UploadURL_key(ctx context.Context, field graphql.CollectedField, obj *UploadURL) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_UploadURL_key(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.Key, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_UploadURL_key(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "UploadURL",
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) _UploadURL_expiresAt(ctx context.Context, field graphql.CollectedField, obj *UploadURL) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_UploadURL_expiresAt(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.ExpiresAt, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_UploadURL_expiresAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "UploadURL",
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) _User_id(ctx context.Context, field graphql.CollectedField, obj *domain.User) (ret graphql.Marshaler) { func (ec *executionContext) _User_id(ctx context.Context, field graphql.CollectedField, obj *domain.User) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_User_id(ctx, field) fc, err := ec.fieldContext_User_id(ctx, field)
if err != nil { if err != nil {
@ -11105,6 +11348,40 @@ func (ec *executionContext) fieldContext___Type_isOneOf(_ context.Context, field
// region **************************** input.gotpl ***************************** // region **************************** input.gotpl *****************************
func (ec *executionContext) unmarshalInputCreatePostInput(ctx context.Context, obj any) (CreatePostInput, error) {
var it CreatePostInput
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"title", "s3TempKey"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
case "title":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("title"))
data, err := ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
it.Title = data
case "s3TempKey":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("s3TempKey"))
data, err := ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
it.S3TempKey = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj any) (domain.LoginInput, error) { func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj any) (domain.LoginInput, error) {
var it domain.LoginInput var it domain.LoginInput
asMap := map[string]any{} asMap := map[string]any{}
@ -12796,6 +13073,28 @@ 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 "generateUploadURL":
field := field
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Query_generateUploadURL(ctx, field)
if res == graphql.Null {
atomic.AddUint32(&fs.Invalids, 1)
}
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 "getUserPosts": case "getUserPosts":
field := field field := field
@ -13511,6 +13810,55 @@ func (ec *executionContext) _UnfollowResult(ctx context.Context, sel ast.Selecti
return out return out
} }
var uploadURLImplementors = []string{"UploadURL"}
func (ec *executionContext) _UploadURL(ctx context.Context, sel ast.SelectionSet, obj *UploadURL) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, uploadURLImplementors)
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("UploadURL")
case "url":
out.Values[i] = ec._UploadURL_url(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "key":
out.Values[i] = ec._UploadURL_key(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "expiresAt":
out.Values[i] = ec._UploadURL_expiresAt(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 userImplementors = []string{"User"} var userImplementors = []string{"User"}
func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *domain.User) graphql.Marshaler { func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *domain.User) graphql.Marshaler {
@ -14353,6 +14701,11 @@ func (ec *executionContext) marshalNComment2ᚖtailly_back_v2ᚋinternalᚋdomai
return ec._Comment(ctx, sel, v) return ec._Comment(ctx, sel, v)
} }
func (ec *executionContext) unmarshalNCreatePostInput2tailly_back_v2ᚋinternalᚋhttpᚋgraphᚐCreatePostInput(ctx context.Context, v any) (CreatePostInput, error) {
res, err := ec.unmarshalInputCreatePostInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNDevice2tailly_back_v2ᚋinternalᚋdomainᚐDevice(ctx context.Context, sel ast.SelectionSet, v domain.Device) graphql.Marshaler { func (ec *executionContext) marshalNDevice2tailly_back_v2ᚋinternalᚋdomainᚐDevice(ctx context.Context, sel ast.SelectionSet, v domain.Device) graphql.Marshaler {
return ec._Device(ctx, sel, &v) return ec._Device(ctx, sel, &v)
} }
@ -14907,6 +15260,20 @@ func (ec *executionContext) marshalNUnfollowResult2ᚖtailly_back_v2ᚋinternal
return ec._UnfollowResult(ctx, sel, v) return ec._UnfollowResult(ctx, sel, v)
} }
func (ec *executionContext) marshalNUploadURL2tailly_back_v2ᚋinternalᚋhttpᚋgraphᚐUploadURL(ctx context.Context, sel ast.SelectionSet, v UploadURL) graphql.Marshaler {
return ec._UploadURL(ctx, sel, &v)
}
func (ec *executionContext) marshalNUploadURL2ᚖtailly_back_v2ᚋinternalᚋhttpᚋgraphᚐUploadURL(ctx context.Context, sel ast.SelectionSet, v *UploadURL) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
return graphql.Null
}
return ec._UploadURL(ctx, sel, v)
}
func (ec *executionContext) marshalNUser2tailly_back_v2ᚋinternalᚋdomainᚐUser(ctx context.Context, sel ast.SelectionSet, v domain.User) graphql.Marshaler { func (ec *executionContext) marshalNUser2tailly_back_v2ᚋinternalᚋdomainᚐUser(ctx context.Context, sel ast.SelectionSet, v domain.User) graphql.Marshaler {
return ec._User(ctx, sel, &v) return ec._User(ctx, sel, &v)
} }

View File

@ -9,6 +9,11 @@ import (
"strconv" "strconv"
) )
type CreatePostInput struct {
Title string `json:"title"`
S3TempKey string `json:"s3TempKey"`
}
type FollowResult struct { type FollowResult struct {
Success bool `json:"success"` Success bool `json:"success"`
Message string `json:"message"` Message string `json:"message"`
@ -73,6 +78,12 @@ type UnfollowResult struct {
Message string `json:"message"` Message string `json:"message"`
} }
type UploadURL struct {
URL string `json:"url"`
Key string `json:"key"`
ExpiresAt string `json:"expiresAt"`
}
type MessageStatus string type MessageStatus string
const ( const (

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"tailly_back_v2/internal/domain" "tailly_back_v2/internal/domain"
"tailly_back_v2/pkg/S3"
"time" "time"
) )
@ -124,19 +125,38 @@ func (r *mutationResolver) UnlikePost(ctx context.Context, postID int) (bool, er
} }
// CreatePost is the resolver for the createPost field. // CreatePost is the resolver for the createPost field.
func (r *mutationResolver) CreatePost(ctx context.Context, title string, content string) (*domain.Post, error) { func (r *mutationResolver) CreatePost(ctx context.Context, input CreatePostInput) (*domain.Post, error) {
userID, err := getUserIDFromContext(ctx) userID, err := getUserIDFromContext(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("unauthorized: %w", err) return nil, fmt.Errorf("unauthorized: %w", err)
} }
post, err := r.Services.Post.Create(ctx, userID, title, content) post, err := r.Services.Post.CreateWithS3Upload(ctx, userID, input.Title, input.S3TempKey)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create post: %w", err) return nil, fmt.Errorf("failed to create post: %w", err)
} }
return post, nil return post, nil
} }
// GenerateUploadURL is the resolver for the generateUploadURL field.
func (r *queryResolver) GenerateUploadURL(ctx context.Context, filename string) (*UploadURL, error) {
userID, err := getUserIDFromContext(ctx)
if err != nil {
return nil, fmt.Errorf("unauthorized: %w", err)
}
url, key, err := S3.GeneratePresignedUploadURL(userID, filename)
if err != nil {
return nil, fmt.Errorf("failed to generate upload URL: %w", err)
}
return &UploadURL{
URL: url,
Key: key,
ExpiresAt: time.Now().Add(15 * time.Minute).Format(time.RFC3339),
}, nil
}
// DeletePost is the resolver for the deletePost field. // DeletePost is the resolver for the deletePost field.
func (r *mutationResolver) DeletePost(ctx context.Context, id int) (bool, error) { func (r *mutationResolver) DeletePost(ctx context.Context, id int) (bool, error) {
userID, err := getUserIDFromContext(ctx) userID, err := getUserIDFromContext(ctx)

View File

@ -170,12 +170,26 @@ type MarkNotificationReadResult {
message: String! message: String!
} }
scalar Upload
input CreatePostInput {
title: String!
s3TempKey: String!
}
type UploadURL {
url: String!
key: String!
expiresAt: String!
}
# Запросы (получение данных) # Запросы (получение данных)
type Query { type Query {
me: User! # Получить текущего пользователя me: User! # Получить текущего пользователя
post(id: Int!): Post! # Получить пост по ID post(id: Int!): Post! # Получить пост по ID
posts: [Post!]! # Получить все посты posts: [Post!]! # Получить все посты
postsPaginated(limit: Int!, offset: Int!): [Post!]! postsPaginated(limit: Int!, offset: Int!): [Post!]!
generateUploadURL(filename: String!): UploadURL!
getUserPosts(userId: Int!): [Post!]! getUserPosts(userId: Int!): [Post!]!
user(id: Int!): User! # Получить пользователя по ID user(id: Int!): User! # Получить пользователя по ID
users: [User!]! users: [User!]!
@ -186,19 +200,14 @@ type Query {
comments(postID: Int!): [Comment!]! comments(postID: Int!): [Comment!]!
# Получить количество подписчиков # Получить количество подписчиков
getFollowersCount(userId: Int!): Int! getFollowersCount(userId: Int!): Int!
# Получить количество подписок # Получить количество подписок
getFollowingCount(userId: Int!): Int! getFollowingCount(userId: Int!): Int!
# Проверить подписку # Проверить подписку
isFollowing(followingId: Int!): Boolean! isFollowing(followingId: Int!): Boolean!
# Получить список подписчиков # Получить список подписчиков
getFollowers(userId: Int!, limit: Int = 20, offset: Int = 0): FollowersResponse! getFollowers(userId: Int!, limit: Int = 20, offset: Int = 0): FollowersResponse!
# Получить список подписок # Получить список подписок
getFollowing(userId: Int!, limit: Int = 20, offset: Int = 0): FollowingResponse! getFollowing(userId: Int!, limit: Int = 20, offset: Int = 0): FollowingResponse!
# Получить уведомления о подписках # Получить уведомления о подписках
getSubscriptionNotifications( getSubscriptionNotifications(
unreadOnly: Boolean = false unreadOnly: Boolean = false
@ -219,8 +228,7 @@ type Mutation {
refreshTokens(refreshToken: String!): Tokens! refreshTokens(refreshToken: String!): Tokens!
# Создание поста # Создание поста
createPost(title: String!, content: String!): Post! createPost(input: CreatePostInput!): Post!
# Создание комментария # Создание комментария
createComment(postId: Int!, content: String!): Comment! createComment(postId: Int!, content: String!): Comment!

View File

@ -32,6 +32,7 @@ type PostService interface {
Update(ctx context.Context, id int, title, content string) (*domain.Post, error) Update(ctx context.Context, id int, title, content string) (*domain.Post, error)
Delete(ctx context.Context, id int) error Delete(ctx context.Context, id int) error
GetPaginated(ctx context.Context, limit, offset int) ([]*domain.Post, error) GetPaginated(ctx context.Context, limit, offset int) ([]*domain.Post, error)
CreateWithS3Upload(ctx context.Context, authorID int, title, s3TempKey string) (*domain.Post, error)
} }
// Реализация сервиса постов // Реализация сервиса постов
@ -44,6 +45,80 @@ func NewPostService(postRepo repository.PostRepository) PostService {
return &postService{postRepo: postRepo} return &postService{postRepo: postRepo}
} }
func (s *postService) CreateWithS3Upload(ctx context.Context, authorID int, title, s3TempKey string) (*domain.Post, error) {
const op = "service/postService.CreateWithS3Upload"
// Валидация
if s3TempKey == "" {
return nil, errors.New("S3 temp key cannot be empty")
}
// Проверяем что ключ принадлежит пользователю (дополнительная валидация)
if !isValidUserTempKey(authorID, s3TempKey) {
return nil, errors.New("invalid temp key for user")
}
// Генерируем временный URL для модерации
tempURL := fmt.Sprintf("https://s3.regru.cloud/tailly/%s", s3TempKey)
// Модерация изображения по URL
modClient, err := moderation.NewModerationClient("tailly_censor:50051")
if err != nil {
return nil, fmt.Errorf("%s: failed to create moderation client: %w", op, err)
}
defer modClient.Close()
allowed, err := modClient.CheckImageURL(ctx, tempURL)
if err != nil {
// Удаляем временный файл при ошибке модерации
S3.DeleteTempFile(s3TempKey)
return nil, fmt.Errorf("%s: image moderation failed: %w", op, err)
}
if !allowed {
// Удаляем временный файл если не прошло модерацию
S3.DeleteTempFile(s3TempKey)
return nil, errors.New("image rejected by moderation service")
}
// Перемещаем файл из временной папки в постоянную
permanentKey := fmt.Sprintf("posts/%d/%d_%s", authorID, time.Now().UnixNano(), getFilenameFromKey(s3TempKey))
err = S3.MoveFile(s3TempKey, permanentKey)
if err != nil {
S3.DeleteTempFile(s3TempKey) // Удаляем временный файл при ошибке
return nil, fmt.Errorf("%s: failed to move file to permanent location: %w", op, err)
}
imageURL := fmt.Sprintf("https://s3.regru.cloud/tailly/%s", permanentKey)
post := &domain.Post{
Title: title,
Content: imageURL,
AuthorID: authorID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.postRepo.Create(ctx, post); err != nil {
// Если не удалось сохранить пост, удаляем файл из S3
S3.DeleteTempFile(permanentKey)
return nil, fmt.Errorf("%s: failed to save post: %w", op, err)
}
return post, nil
}
func isValidUserTempKey(userID int, key string) bool {
// Проверяем что ключ принадлежит пользователю
// Формат: temp/{userID}/{timestamp}_{filename}
return strings.Contains(key, fmt.Sprintf("temp/%d/", userID))
}
func getFilenameFromKey(key string) string {
parts := strings.Split(key, "/")
return parts[len(parts)-1]
}
// Создание нового поста // Создание нового поста
func (s *postService) Create(ctx context.Context, authorID int, title, content string) (*domain.Post, error) { func (s *postService) Create(ctx context.Context, authorID int, title, content string) (*domain.Post, error) {
const op = "service/postService.Create" const op = "service/postService.Create"

View File

@ -2,11 +2,13 @@ package S3
import ( import (
"fmt" "fmt"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/s3"
"strings"
) )
func DeleteFromS3(imageURL string) error { func DeleteFromS3(imageURL string) error {
@ -50,3 +52,90 @@ func DeleteFromS3(imageURL string) error {
return nil return nil
} }
func GeneratePresignedUploadURL(userID int, filename string) (string, string, error) {
const op = "s3.GeneratePresignedUploadURL"
sess := session.Must(session.NewSession(&aws.Config{
Region: aws.String("ru-central1"),
Endpoint: aws.String("https://s3.regru.cloud"),
Credentials: credentials.NewStaticCredentials(
"TJ946G2S1Z5FEI3I7DQQ",
"C2H2aITHRDpek8H921yhnrINZwDoADsjW3F6HURl",
"",
),
}))
svc := s3.New(sess)
// Генерируем уникальный ключ с userID для безопасности
uniqueKey := fmt.Sprintf("temp/%d/%d_%s", userID, time.Now().UnixNano(), filename)
req, _ := svc.PutObjectRequest(&s3.PutObjectInput{
Bucket: aws.String("tailly"),
Key: aws.String(uniqueKey),
ContentLength: aws.Int64(10 * 1024 * 1024), // Макс 10MB
})
// URL действителен 15 минут
url, err := req.Presign(15 * time.Minute)
if err != nil {
return "", "", fmt.Errorf("%s: failed to generate presigned URL: %w", op, err)
}
return url, uniqueKey, nil
}
func MoveFile(sourceKey, destinationKey string) error {
const op = "s3.MoveFile"
sess := session.Must(session.NewSession(&aws.Config{
Region: aws.String("ru-central1"),
Endpoint: aws.String("https://s3.regru.cloud"),
Credentials: credentials.NewStaticCredentials(
"TJ946G2S1Z5FEI3I7DQQ",
"C2H2aITHRDpek8H921yhnrINZwDoADsjW3F6HURl",
"",
),
}))
svc := s3.New(sess)
// Копируем файл
_, err := svc.CopyObject(&s3.CopyObjectInput{
Bucket: aws.String("tailly"),
CopySource: aws.String(fmt.Sprintf("tailly/%s", sourceKey)),
Key: aws.String(destinationKey),
})
if err != nil {
return fmt.Errorf("%s: failed to copy file: %w", op, err)
}
// Удаляем исходный файл
return DeleteTempFile(sourceKey)
}
func DeleteTempFile(key string) error {
const op = "s3.DeleteTempFile"
sess := session.Must(session.NewSession(&aws.Config{
Region: aws.String("ru-central1"),
Endpoint: aws.String("https://s3.regru.cloud"),
Credentials: credentials.NewStaticCredentials(
"TJ946G2S1Z5FEI3I7DQQ",
"C2H2aITHRDpek8H921yhnrINZwDoADsjW3F6HURl",
"",
),
}))
svc := s3.New(sess)
_, err := svc.DeleteObject(&s3.DeleteObjectInput{
Bucket: aws.String("tailly"),
Key: aws.String(key),
})
if err != nil {
return fmt.Errorf("%s: failed to delete temp file: %w", op, err)
}
return nil
}

View File

@ -3,8 +3,9 @@ package moderation
import ( import (
"context" "context"
"google.golang.org/grpc"
pb "tailly_back_v2/pkg/moderation/proto" pb "tailly_back_v2/pkg/moderation/proto"
"google.golang.org/grpc"
) )
type ModerationClient struct { type ModerationClient struct {
@ -35,6 +36,16 @@ func (c *ModerationClient) CheckImage(ctx context.Context, imageData []byte) (bo
return resp.OverallDecision == "allowed", nil return resp.OverallDecision == "allowed", nil
} }
func (c *ModerationClient) CheckImageURL(ctx context.Context, imageURL string) (bool, error) {
resp, err := c.client.CheckImage(ctx, &pb.ImageRequest{
ImageUrl: imageURL, // ← Передаем URL вместо данных!
})
if err != nil {
return false, err
}
return resp.OverallDecision == "allowed", nil
}
func (c *ModerationClient) Close() { func (c *ModerationClient) Close() {
c.conn.Close() c.conn.Close()
} }