Skip to content

Commit 4265031

Browse files
authored
Feat/v0.11.0 (#53)
1 parent fc566bc commit 4265031

File tree

19 files changed

+378
-287
lines changed

19 files changed

+378
-287
lines changed

backend/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/chaitin/koalaqa
33
go 1.25.0
44

55
require (
6+
github.com/Narasimha1997/ratelimiter v1.1.1
67
github.com/caarlos0/env/v11 v11.3.1
78
github.com/chaitin/ModelKit v1.8.3
89
github.com/chaitin/pandawiki/sdk/rag v0.0.0-20250927130416-bcfc4bde3379

backend/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.3 h1:r3fokGFRDk/8pHmwLwJ8zs
1616
github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.3/go.mod h1:HtsP+1Fchp4dVvaiIsLHAl/yqL3H1YLwqLC9kNwqQEg=
1717
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
1818
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
19+
github.com/Narasimha1997/ratelimiter v1.1.1 h1:ndkK0dHqUKdwSElE8Kghz+0gVcGEa9q6/CisEL/h6HU=
20+
github.com/Narasimha1997/ratelimiter v1.1.1/go.mod h1:TCsPmcx5vkQJu64sbTLRcr8xpNNmO22OTnvhfXEWoNw=
1921
github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
2022
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
2123
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package migration
2+
3+
import (
4+
"gorm.io/gorm"
5+
6+
"github.com/chaitin/koalaqa/migration/migrator"
7+
)
8+
9+
type updateDiscussionMembers struct{}
10+
11+
func (m *updateDiscussionMembers) Version() int64 {
12+
return 20251029101746
13+
}
14+
15+
func (m *updateDiscussionMembers) Migrate(tx *gorm.DB) error {
16+
return tx.Exec(`UPDATE discussions SET members = ARRAY_REMOVE(ARRAY_APPEND(comm.user_ids, user_id), 0)
17+
FROM (SELECT discussion_id, ARRAY_AGG(distinct user_id) AS user_ids FROM comments GROUP BY discussion_id) AS comm
18+
WHERE discussions.id = comm.discussion_id;`).Error
19+
}
20+
21+
func newUpdateDiscussionMembers() migrator.Migrator {
22+
return &updateDiscussionMembers{}
23+
}
24+
25+
func init() {
26+
registerDBMigrator(newUpdateDiscussionMembers)
27+
}

backend/pkg/webhook/message/discuss.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package message
22

33
import (
44
"bytes"
5+
"strconv"
56
"strings"
67
"text/template"
78
"time"
@@ -39,6 +40,10 @@ type sendDiscussMsg struct {
3940
platformMsg
4041
}
4142

43+
func (d *discussMsg) ID() string {
44+
return strconv.FormatUint(uint64(d.Common.Discussion.ID), 10)
45+
}
46+
4247
func (d *discussMsg) Message(webhookType model.WebhookType) (string, error) {
4348
var buff bytes.Buffer
4449
err := discussTpl.Execute(&buff, sendDiscussMsg{

backend/pkg/webhook/message/message.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ type Common struct {
6262
}
6363

6464
type Message interface {
65+
ID() string
6566
Type() Type
6667
Title() string
6768
Message(webhookType model.WebhookType) (string, error)

backend/pkg/webhook/message/qa.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package message
22

33
import (
44
"bytes"
5+
"strconv"
56
"text/template"
67
"time"
78

@@ -38,6 +39,10 @@ type sendQAMsg struct {
3839
platformMsg
3940
}
4041

42+
func (q *docMsg) ID() string {
43+
return strconv.FormatUint(uint64(q.Doc.ID), 10)
44+
}
45+
4146
func (q *docMsg) Message(webhookType model.WebhookType) (string, error) {
4247
var buff bytes.Buffer
4348
err := docTpl.Execute(&buff, sendQAMsg{
@@ -70,7 +75,7 @@ func NewQANeedReview(body commonDoc) Message {
7075
return &docMsg{
7176
Header: Header{
7277
MsgType: TypeQANeedReview,
73-
MsgTitle: "有新的问答等待审核",
78+
MsgTitle: "后台有新的回答等待审核",
7479
HeadingPrefix: "问题",
7580
},
7681
Doc: body,

backend/router/discussion_auth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ func (d *discussionAuth) RevokeCommentLike(ctx *context.Context) {
293293
return
294294
}
295295

296-
err = d.disc.RevokeLike(ctx, ctx.GetUser().UID, commentID)
296+
err = d.disc.RevokeLike(ctx, ctx.GetUser().UID, ctx.Param("disc_id"), commentID)
297297
if err != nil {
298298
ctx.InternalError(err, "dislike comment failed")
299299
return

backend/svc/discussion.go

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package svc
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
67
"fmt"
78
"mime/multipart"
89
"path/filepath"
910
"time"
1011

12+
"github.com/Narasimha1997/ratelimiter"
1113
"github.com/chaitin/koalaqa/model"
1214
"github.com/chaitin/koalaqa/pkg/glog"
1315
"github.com/chaitin/koalaqa/pkg/mq"
@@ -41,6 +43,7 @@ type Discussion struct {
4143

4244
logger *glog.Logger
4345
webhookType map[model.DiscussionType]message.Type
46+
limiter *ratelimiter.AttributeBasedLimiter
4447
}
4548

4649
func newDiscussion(in discussionIn) *Discussion {
@@ -52,6 +55,7 @@ func newDiscussion(in discussionIn) *Discussion {
5255
model.DiscussionTypeFeedback: message.TypeNewFeedback,
5356
model.DiscussionTypeBlog: message.TypeNewBlog,
5457
},
58+
limiter: ratelimiter.NewAttributeBasedLimiter(false),
5559
}
5660
}
5761

@@ -73,7 +77,30 @@ func (d *Discussion) generateUUID() string {
7377
return util.RandomString(16)
7478
}
7579

80+
func (d *Discussion) limitKey(args ...any) string {
81+
if len(args) == 0 {
82+
return ""
83+
}
84+
85+
buff := bytes.NewBufferString("%v")
86+
for i := 1; i < len(args); i++ {
87+
buff.WriteString("-%v")
88+
}
89+
90+
return fmt.Sprintf(buff.String(), args...)
91+
}
92+
93+
func (d *Discussion) allow(args ...any) bool {
94+
return d.limiter.MustShouldAllow(d.limitKey(args...), 1, 3, time.Minute)
95+
}
96+
97+
var errRatelimit = errors.New("ratelimit")
98+
7699
func (d *Discussion) Create(ctx context.Context, req DiscussionCreateReq) (string, error) {
100+
if !d.allow("discussion", req.UserID) {
101+
return "", errRatelimit
102+
}
103+
77104
if len(req.GroupIDs) > 0 {
78105
err := d.in.GroupItemRepo.FilterIDs(ctx, &req.GroupIDs)
79106
if err != nil {
@@ -292,7 +319,7 @@ func (d *Discussion) IncrementComment(uuid string, updateTime bool) {
292319
func (d *Discussion) DecrementComment(uuid string) {
293320
ctx := context.Background()
294321
d.in.DiscRepo.Update(ctx, map[string]any{
295-
"comment": gorm.Expr("comment-1"),
322+
"comment": gorm.Expr("CASE WHEN comment>0 THEN comment-1 END"),
296323
}, repo.QueryWithEqual("uuid", uuid))
297324

298325
go d.RecalculateHot(uuid)
@@ -321,6 +348,10 @@ func (d *Discussion) RecalculateHot(uuid string) {
321348
}
322349

323350
func (d *Discussion) LikeDiscussion(ctx context.Context, discUUID string, user model.UserInfo) error {
351+
if !d.allow("like", discUUID, user.UID) {
352+
return errRatelimit
353+
}
354+
324355
if err := d.in.DiscRepo.LikeDiscussion(ctx, discUUID, user.UID); err != nil {
325356
return err
326357
}
@@ -344,6 +375,10 @@ func (d *Discussion) LikeDiscussion(ctx context.Context, discUUID string, user m
344375
}
345376

346377
func (d *Discussion) RevokeLikeDiscussion(ctx context.Context, discUUID string, uid uint) error {
378+
if !d.allow("like", discUUID, uid) {
379+
return errRatelimit
380+
}
381+
347382
if err := d.in.DiscRepo.RevokeLikeDiscussion(ctx, discUUID, uid); err != nil {
348383
return err
349384
}
@@ -406,6 +441,12 @@ type CommentCreateReq struct {
406441
}
407442

408443
func (d *Discussion) CreateComment(ctx context.Context, uid uint, discUUID string, req CommentCreateReq) (uint, error) {
444+
if !req.Bot {
445+
if !d.allow("comment", discUUID, uid) {
446+
return 0, errRatelimit
447+
}
448+
}
449+
409450
disc, err := d.in.DiscRepo.GetByUUID(ctx, discUUID)
410451
if err != nil {
411452
return 0, err
@@ -568,6 +609,10 @@ func (d *Discussion) UploadFile(ctx context.Context, req DiscussUploadFileReq) (
568609
}
569610

570611
func (d *Discussion) AcceptComment(ctx context.Context, user model.UserInfo, discUUID string, commentID uint) error {
612+
if !d.allow("accept", discUUID, user.UID) {
613+
return errRatelimit
614+
}
615+
571616
disc, err := d.in.DiscRepo.GetByUUID(ctx, discUUID)
572617
if err != nil {
573618
return err
@@ -645,6 +690,10 @@ func (d *Discussion) AcceptComment(ctx context.Context, user model.UserInfo, dis
645690
}
646691

647692
func (d *Discussion) LikeComment(ctx context.Context, userInfo model.UserInfo, discUUID string, commentID uint) error {
693+
if !d.allow("like", discUUID, userInfo.UID) {
694+
return errRatelimit
695+
}
696+
648697
disc, err := d.in.DiscRepo.GetByUUID(ctx, discUUID)
649698
if err != nil {
650699
return err
@@ -682,6 +731,10 @@ func (d *Discussion) LikeComment(ctx context.Context, userInfo model.UserInfo, d
682731
}
683732

684733
func (d *Discussion) DislikeComment(ctx context.Context, userInfo model.UserInfo, discUUID string, commentID uint) error {
734+
if !d.allow("like", discUUID, userInfo.UID) {
735+
return errRatelimit
736+
}
737+
685738
disc, err := d.in.DiscRepo.GetByUUID(ctx, discUUID)
686739
if err != nil {
687740
return err
@@ -718,7 +771,11 @@ func (d *Discussion) DislikeComment(ctx context.Context, userInfo model.UserInfo
718771
return nil
719772
}
720773

721-
func (d *Discussion) RevokeLike(ctx context.Context, uid uint, commentID uint) error {
774+
func (d *Discussion) RevokeLike(ctx context.Context, uid uint, discUUID string, commentID uint) error {
775+
if !d.allow("like", discUUID, uid) {
776+
return errRatelimit
777+
}
778+
722779
ok, err := d.in.CommRepo.ExistByID(ctx, commentID)
723780
if err != nil {
724781
return err

backend/svc/webhook.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package svc
22

33
import (
44
"context"
5+
"fmt"
56
"sync"
67
"time"
78

9+
"github.com/Narasimha1997/ratelimiter"
810
"github.com/chaitin/koalaqa/model"
911
"github.com/chaitin/koalaqa/pkg/glog"
1012
"github.com/chaitin/koalaqa/pkg/webhook"
@@ -17,6 +19,7 @@ type Webhook struct {
1719
logger *glog.Logger
1820
lock sync.Mutex
1921
webhooks map[uint]webhook.Webhook
22+
limiter *ratelimiter.AttributeBasedLimiter
2023

2124
repoWebhook *repo.Webhook
2225
}
@@ -121,8 +124,16 @@ func (w *Webhook) Delete(ctx context.Context, id uint) error {
121124
return nil
122125
}
123126

127+
func (w *Webhook) allow(id uint, discID string, msgType message.Type) bool {
128+
return w.limiter.MustShouldAllow(fmt.Sprintf("webhook-%d-%s-%d", id, discID, msgType), 1, 1, time.Minute*5)
129+
}
130+
124131
func (w *Webhook) Send(ctx context.Context, msg message.Message) error {
125132
for id, hook := range w.webhooks {
133+
if !w.allow(id, msg.ID(), msg.Type()) {
134+
w.logger.WithContext(ctx).With("webhook_id", id).With("msg", msg).Debug("webhook ratelimit, skip send")
135+
continue
136+
}
126137
err := hook.Send(ctx, msg)
127138
if err != nil {
128139
w.logger.WithContext(ctx).WithErr(err).With("id", id).Warn("send webhook failed")
@@ -137,6 +148,7 @@ func newWebhook(lc fx.Lifecycle, repoWebhook *repo.Webhook) *Webhook {
137148
logger: glog.Module("svc", "webhook"),
138149
repoWebhook: repoWebhook,
139150
webhooks: make(map[uint]webhook.Webhook),
151+
limiter: ratelimiter.NewAttributeBasedLimiter(false),
140152
}
141153
lc.Append(fx.Hook{
142154
OnStart: func(ctx context.Context) error {

0 commit comments

Comments
 (0)