Skip to content

Commit b1da65f

Browse files
committed
Merge remote-tracking branch 'origin/fix/2.0.1/chat' into test
2 parents 0aca063 + 1109124 commit b1da65f

17 files changed

Lines changed: 386 additions & 82 deletions

internal/cli/build.go

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,27 @@ func createMainGoFile(b *buildingMaterial) (err error) {
211211

212212
// downloadGoModFile run go mod commands to download dependencies
213213
func downloadGoModFile(b *buildingMaterial) (err error) {
214-
// If user specify a module replacement, use it. Otherwise, use the latest version.
215-
if len(b.answerModuleReplacement) > 0 {
216-
replacement := fmt.Sprintf("%s=%s", "github.com/apache/answer", b.answerModuleReplacement)
214+
answerReplacement := b.answerModuleReplacement
215+
216+
// If no replacement specified and current binary is v2+, auto-determine replacement.
217+
// This is needed because go mod tidy would otherwise resolve github.com/apache/answer
218+
// to the latest v1.x version, causing v2+ features (e.g. AI/MCP) to disappear.
219+
if len(answerReplacement) == 0 && b.originalAnswerInfo.Version != "" {
220+
ver, verErr := semver.NewVersion(strings.TrimPrefix(b.originalAnswerInfo.Version, "v"))
221+
if verErr == nil && ver.Major() >= 2 {
222+
answerReplacement = fmt.Sprintf("github.com/apache/answer@%s", b.originalAnswerInfo.Version)
223+
}
224+
}
225+
226+
if len(answerReplacement) > 0 {
227+
// For v2+ versioned module paths (e.g. github.com/apache/answer@v2.0.0),
228+
// go mod tidy rejects the version because the module path lacks a /v2 suffix.
229+
// Work around this by cloning the repo locally and using a local path replacement.
230+
localPath, resolveErr := resolveAnswerModuleReplacement(answerReplacement, b.tmpDir)
231+
if resolveErr != nil {
232+
return resolveErr
233+
}
234+
replacement := fmt.Sprintf("%s=%s", "github.com/apache/answer", localPath)
217235
err = b.newExecCmd("go", "mod", "edit", "-replace", replacement).Run()
218236
if err != nil {
219237
return err
@@ -232,6 +250,56 @@ func downloadGoModFile(b *buildingMaterial) (err error) {
232250
return
233251
}
234252

253+
// resolveAnswerModuleReplacement resolves the ANSWER_MODULE value to a usable local path or
254+
// remote replacement string. For v2+ versioned module paths (e.g. github.com/apache/answer@v2.0.0),
255+
// Go module system rejects the version because the module path has no /v2 suffix. In that case
256+
// the repository is cloned locally and the local path is returned instead.
257+
func resolveAnswerModuleReplacement(replacement, tmpDir string) (string, error) {
258+
// Local paths can be used as-is.
259+
if strings.HasPrefix(replacement, "/") || strings.HasPrefix(replacement, "./") || strings.HasPrefix(replacement, "../") {
260+
return replacement, nil
261+
}
262+
263+
// Parse module@version format.
264+
moduleName, version, hasVersion := strings.Cut(replacement, "@")
265+
if !hasVersion {
266+
return replacement, nil
267+
}
268+
269+
// Only handle v2+ versions on module paths without the /vN suffix.
270+
ver, err := semver.StrictNewVersion(strings.TrimPrefix(version, "v"))
271+
if err != nil || ver.Major() < 2 {
272+
return replacement, nil
273+
}
274+
if strings.HasSuffix(moduleName, fmt.Sprintf("/v%d", ver.Major())) {
275+
return replacement, nil
276+
}
277+
278+
// Clone the repo to a local directory and return its path.
279+
gitURL := "https://" + moduleName
280+
tag := "v" + strings.TrimPrefix(version, "v")
281+
localPath := filepath.Join(filepath.Dir(tmpDir), fmt.Sprintf("answer_src_%s", strings.ReplaceAll(version, ".", "_")))
282+
283+
if _, statErr := os.Stat(localPath); statErr == nil {
284+
fmt.Printf("[build] using cached local clone at %s\n", localPath)
285+
return localPath, nil
286+
}
287+
288+
fmt.Printf("[build] v2+ module detected, cloning %s@%s to local path %s...\n", moduleName, version, localPath)
289+
cloneCmd := exec.Command("git", "clone", "--depth=1", "--branch="+tag, gitURL, localPath)
290+
cloneCmd.Stdout = os.Stdout
291+
cloneCmd.Stderr = os.Stderr
292+
if err = cloneCmd.Run(); err != nil {
293+
return "", fmt.Errorf(
294+
"failed to clone %s@%s: %w\nTip: set ANSWER_MODULE to a local checkout path instead, e.g. ANSWER_MODULE=/path/to/answer",
295+
moduleName, version, err,
296+
)
297+
}
298+
299+
fmt.Printf("[build] successfully cloned to %s\n", localPath)
300+
return localPath, nil
301+
}
302+
235303
// movePluginToVendor move plugin to vendor dir
236304
// Traverse the plugins, and if the plugin path is not github.com/apache/answer-plugins, move the contents of the current plugin to the vendor/github.com/apache/answer-plugins/ directory.
237305
func movePluginToVendor(b *buildingMaterial) (err error) {

internal/controller/answer_controller.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,9 @@ func (ac *AnswerController) GetAnswerInfo(ctx *gin.Context) {
164164
id := ctx.Query("id")
165165
id = uid.DeShortID(id)
166166
userID := middleware.GetLoginUserIDFromContext(ctx)
167+
isAdminModerator := middleware.GetUserIsAdminModerator(ctx)
167168

168-
info, questionInfo, has, err := ac.answerService.Get(ctx, id, userID)
169+
info, questionInfo, has, err := ac.answerService.Get(ctx, id, userID, isAdminModerator)
169170
if err != nil {
170171
handler.HandleResponse(ctx, err, gin.H{})
171172
return
@@ -271,7 +272,7 @@ func (ac *AnswerController) AddAnswer(ctx *gin.Context) {
271272
if !isAdmin || !linkUrlLimitUser {
272273
ac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionAnswer, req.UserID)
273274
}
274-
info, questionInfo, has, err := ac.answerService.Get(ctx, answerID, req.UserID)
275+
info, questionInfo, has, err := ac.answerService.Get(ctx, answerID, req.UserID, isAdmin)
275276
if err != nil {
276277
handler.HandleResponse(ctx, err, nil)
277278
return
@@ -348,7 +349,7 @@ func (ac *AnswerController) UpdateAnswer(ctx *gin.Context) {
348349
if !isAdmin || !linkUrlLimitUser {
349350
ac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID)
350351
}
351-
_, _, _, err = ac.answerService.Get(ctx, req.ID, req.UserID)
352+
_, _, _, err = ac.answerService.Get(ctx, req.ID, req.UserID, isAdmin)
352353
if err != nil {
353354
handler.HandleResponse(ctx, err, nil)
354355
return
@@ -376,6 +377,7 @@ func (ac *AnswerController) AnswerList(ctx *gin.Context) {
376377

377378
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
378379
req.QuestionID = uid.DeShortID(req.QuestionID)
380+
req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx)
379381

380382
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
381383
permission.AnswerEdit,

internal/controller/comment_controller.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ func (cc *CommentController) GetCommentWithPage(ctx *gin.Context) {
248248
req.ObjectID = uid.DeShortID(req.ObjectID)
249249
req.CommentID = uid.DeShortID(req.CommentID)
250250
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
251+
req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx)
251252
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
252253
permission.CommentEdit,
253254
permission.CommentDelete,
@@ -300,6 +301,7 @@ func (cc *CommentController) GetComment(ctx *gin.Context) {
300301
}
301302

302303
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
304+
req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx)
303305
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
304306
permission.CommentEdit,
305307
permission.CommentDelete,

internal/controller/question_controller.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
234234
id = uid.DeShortID(id)
235235
userID := middleware.GetLoginUserIDFromContext(ctx)
236236
req := schema.QuestionPermission{}
237+
req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx)
237238
canList, err := qc.rankService.CheckOperationPermissions(ctx, userID, []string{
238239
permission.QuestionEdit,
239240
permission.QuestionDelete,
@@ -590,7 +591,7 @@ func (qc *QuestionController) AddQuestionByAnswer(ctx *gin.Context) {
590591
handler.HandleResponse(ctx, err, nil)
591592
return
592593
}
593-
info, questionInfo, has, err := qc.answerService.Get(ctx, answerID, req.UserID)
594+
info, questionInfo, has, err := qc.answerService.Get(ctx, answerID, req.UserID, isAdmin)
594595
if err != nil {
595596
handler.HandleResponse(ctx, err, nil)
596597
return

internal/schema/answer_schema.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,16 @@ type AnswerUpdateResp struct {
106106
}
107107

108108
type AnswerListReq struct {
109-
QuestionID string `json:"question_id" form:"question_id"`
110-
Order string `json:"order" form:"order"`
111-
Page int `json:"page" form:"page"`
112-
PageSize int `json:"page_size" form:"page_size"`
113-
UserID string `json:"-"`
114-
IsAdmin bool `json:"-"`
115-
CanEdit bool `json:"-"`
116-
CanDelete bool `json:"-"`
117-
CanRecover bool `json:"-"`
109+
QuestionID string `json:"question_id" form:"question_id"`
110+
Order string `json:"order" form:"order"`
111+
Page int `json:"page" form:"page"`
112+
PageSize int `json:"page_size" form:"page_size"`
113+
UserID string `json:"-"`
114+
IsAdmin bool `json:"-"`
115+
IsAdminModerator bool `json:"-"`
116+
CanEdit bool `json:"-"`
117+
CanDelete bool `json:"-"`
118+
CanRecover bool `json:"-"`
118119
}
119120

120121
type AnswerInfo struct {

internal/schema/comment_schema.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ type GetCommentWithPageReq struct {
150150
// query condition
151151
QueryCond string `validate:"omitempty,oneof=vote created_at" form:"query_cond"`
152152
// user id
153-
UserID string `json:"-"`
153+
UserID string `json:"-"`
154+
IsAdminModerator bool `json:"-"`
154155
// whether user can edit it
155156
CanEdit bool `json:"-"`
156157
// whether user can delete it
@@ -162,7 +163,8 @@ type GetCommentReq struct {
162163
// object id
163164
ID string `validate:"required" form:"id"`
164165
// user id
165-
UserID string `json:"-"`
166+
UserID string `json:"-"`
167+
IsAdminModerator bool `json:"-"`
166168
// whether user can edit it
167169
CanEdit bool `json:"-"`
168170
// whether user can delete it

internal/schema/question_schema.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ func (req *QuestionAddByAnswer) Check() (errFields []*validator.FormErrorField,
143143
}
144144

145145
type QuestionPermission struct {
146+
IsAdminModerator bool `json:"-"`
146147
// whether user can add it
147148
CanAdd bool `json:"-"`
148149
// whether user can edit it

internal/schema/simple_obj_info_schema.go

Lines changed: 99 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,28 @@ package schema
2121

2222
import (
2323
"github.com/apache/answer/internal/base/constant"
24+
"github.com/apache/answer/internal/base/reason"
2425
"github.com/apache/answer/internal/entity"
26+
"github.com/segmentfault/pacman/errors"
2527
)
2628

2729
// SimpleObjectInfo simple object info
2830
type SimpleObjectInfo struct {
29-
ObjectID string `json:"object_id"`
30-
ObjectCreatorUserID string `json:"object_creator_user_id"`
31-
QuestionID string `json:"question_id"`
32-
QuestionStatus int `json:"question_status"`
33-
QuestionShow int `json:"question_show"`
34-
AnswerID string `json:"answer_id"`
35-
AnswerStatus int `json:"answer_status"`
36-
CommentID string `json:"comment_id"`
37-
CommentStatus int `json:"comment_status"`
38-
TagID string `json:"tag_id"`
39-
TagStatus int `json:"tag_status"`
40-
ObjectType string `json:"object_type"`
41-
Title string `json:"title"`
42-
Content string `json:"content"`
31+
ObjectID string `json:"object_id"`
32+
ObjectCreatorUserID string `json:"object_creator_user_id"`
33+
QuestionID string `json:"question_id"`
34+
QuestionCreatorUserID string `json:"question_creator_user_id"`
35+
QuestionStatus int `json:"question_status"`
36+
QuestionShow int `json:"question_show"`
37+
AnswerID string `json:"answer_id"`
38+
AnswerStatus int `json:"answer_status"`
39+
CommentID string `json:"comment_id"`
40+
CommentStatus int `json:"comment_status"`
41+
TagID string `json:"tag_id"`
42+
TagStatus int `json:"tag_status"`
43+
ObjectType string `json:"object_type"`
44+
Title string `json:"title"`
45+
Content string `json:"content"`
4346
}
4447

4548
// IsDeleted is deleted
@@ -57,6 +60,88 @@ func (s *SimpleObjectInfo) IsDeleted() bool {
5760
return false
5861
}
5962

63+
func (s *SimpleObjectInfo) CheckVisibility(userID string, isAdminModerator bool) error {
64+
if s == nil {
65+
return errors.NotFound(reason.ObjectNotFound)
66+
}
67+
if s.isObjectRestricted() && !s.canViewObject(userID, isAdminModerator) {
68+
return errors.NotFound(s.objectNotFoundReason())
69+
}
70+
if s.hasParentQuestion() && s.isParentQuestionRestricted() &&
71+
!s.canViewParentQuestion(userID, isAdminModerator) {
72+
return errors.NotFound(reason.QuestionNotFound)
73+
}
74+
return nil
75+
}
76+
77+
func (s *SimpleObjectInfo) canViewObject(userID string, isAdminModerator bool) bool {
78+
if isAdminModerator {
79+
return true
80+
}
81+
switch s.ObjectType {
82+
case constant.QuestionObjectType:
83+
return s.QuestionCreatorUserID == userID
84+
case constant.AnswerObjectType, constant.CommentObjectType, constant.TagObjectType:
85+
return s.ObjectCreatorUserID == userID
86+
default:
87+
return false
88+
}
89+
}
90+
91+
func (s *SimpleObjectInfo) canViewParentQuestion(userID string, isAdminModerator bool) bool {
92+
if isAdminModerator {
93+
return true
94+
}
95+
return s.QuestionCreatorUserID == userID
96+
}
97+
98+
func (s *SimpleObjectInfo) hasParentQuestion() bool {
99+
switch s.ObjectType {
100+
case constant.AnswerObjectType, constant.CommentObjectType:
101+
return len(s.QuestionID) > 0 && s.QuestionID != "0"
102+
default:
103+
return false
104+
}
105+
}
106+
107+
func (s *SimpleObjectInfo) isObjectRestricted() bool {
108+
switch s.ObjectType {
109+
case constant.QuestionObjectType:
110+
return s.QuestionStatus == entity.QuestionStatusDeleted ||
111+
s.QuestionStatus == entity.QuestionStatusPending ||
112+
s.QuestionShow == entity.QuestionHide
113+
case constant.AnswerObjectType:
114+
return s.AnswerStatus == entity.AnswerStatusDeleted || s.AnswerStatus == entity.AnswerStatusPending
115+
case constant.CommentObjectType:
116+
return s.CommentStatus == entity.CommentStatusDeleted || s.CommentStatus == entity.CommentStatusPending
117+
case constant.TagObjectType:
118+
return s.TagStatus == entity.TagStatusDeleted
119+
default:
120+
return false
121+
}
122+
}
123+
124+
func (s *SimpleObjectInfo) isParentQuestionRestricted() bool {
125+
return s.QuestionStatus == entity.QuestionStatusDeleted ||
126+
s.QuestionStatus == entity.QuestionStatusPending ||
127+
s.QuestionShow == entity.QuestionHide
128+
}
129+
130+
func (s *SimpleObjectInfo) objectNotFoundReason() string {
131+
switch s.ObjectType {
132+
case constant.QuestionObjectType:
133+
return reason.QuestionNotFound
134+
case constant.AnswerObjectType:
135+
return reason.AnswerNotFound
136+
case constant.CommentObjectType:
137+
return reason.CommentNotFound
138+
case constant.TagObjectType:
139+
return reason.TagNotFound
140+
default:
141+
return reason.ObjectNotFound
142+
}
143+
}
144+
60145
type UnreviewedRevisionInfoInfo struct {
61146
CreatedAt int64 `json:"created_at"`
62147
ObjectID string `json:"object_id"`

internal/service/comment/comment_service.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,13 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment
348348
if !exist {
349349
return nil, errors.BadRequest(reason.CommentNotFound)
350350
}
351+
objInfo, err := cs.objectInfoService.GetInfo(ctx, comment.ObjectID)
352+
if err != nil {
353+
return nil, err
354+
}
355+
if err := objInfo.CheckVisibility(req.UserID, req.IsAdminModerator); err != nil {
356+
return nil, err
357+
}
351358

352359
resp = &schema.GetCommentResp{
353360
CommentID: comment.ID,
@@ -399,6 +406,13 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment
399406
// GetCommentWithPage get comment list page
400407
func (cs *CommentService) GetCommentWithPage(ctx context.Context, req *schema.GetCommentWithPageReq) (
401408
pageModel *pager.PageModel, err error) {
409+
objInfo, err := cs.objectInfoService.GetInfo(ctx, req.ObjectID)
410+
if err != nil {
411+
return nil, err
412+
}
413+
if err := objInfo.CheckVisibility(req.UserID, req.IsAdminModerator); err != nil {
414+
return nil, err
415+
}
402416
dto := &CommentQuery{
403417
PageCond: pager.PageCond{Page: req.Page, PageSize: req.PageSize},
404418
ObjectID: req.ObjectID,

0 commit comments

Comments
 (0)