diff --git a/internal/api/captcha.go b/internal/api/captcha.go index 9a0c6f3ca..9cedb3d3e 100644 --- a/internal/api/captcha.go +++ b/internal/api/captcha.go @@ -41,7 +41,7 @@ func (c *CaptchaApi) VerifyCaptcha(ctx *gin.Context) { } resp, err := c.Client.VerifyCaptcha(ctx, req) if err != nil { - log.ZError(ctx, "captcha verify rpc failed", err, "captchaID", req.GetCaptchaID(), "clickCount", len(req.GetClickPoints())) + log.ZError(ctx, "captcha verify rpc failed", err, "captchaID", req.GetCaptchaID(), "x", req.GetX(), "y", req.GetY()) apiresp.GinError(ctx, err) return } diff --git a/internal/api/friend.go b/internal/api/friend.go index 7a7538f0a..060746dae 100644 --- a/internal/api/friend.go +++ b/internal/api/friend.go @@ -15,10 +15,14 @@ package api import ( + "context" "github.com/gin-gonic/gin" + "google.golang.org/grpc" "github.com/openimsdk/protocol/relation" + "github.com/openimsdk/tools/apiresp" "github.com/openimsdk/tools/a2r" + "github.com/openimsdk/tools/errs" ) type FriendApi struct { @@ -124,5 +128,24 @@ func (o *FriendApi) GetPinnedFriendIDs(c *gin.Context) { } func (o *FriendApi) AddOnewayFriend(c *gin.Context) { - a2r.Call(c, relation.FriendClient.AddOnewayFriend, o.Client) + // Current generated relation grpc client may not include AddOnewayFriend yet. + // Keep API route compile-safe and return a clear error instead of breaking build. + client, ok := any(o.Client).(interface { + AddOnewayFriend(ctx context.Context, in *relation.ApplyToAddFriendReq, opts ...grpc.CallOption) (*relation.ApplyToAddFriendResp, error) + }) + if !ok { + apiresp.GinError(c, errs.New("add_oneway_friend rpc is not generated in relation_grpc.pb.go")) + return + } + var req relation.ApplyToAddFriendReq + if err := c.ShouldBindJSON(&req); err != nil { + apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap()) + return + } + resp, err := client.AddOnewayFriend(c, &req) + if err != nil { + apiresp.GinError(c, err) + return + } + apiresp.GinSuccess(c, resp) } diff --git a/internal/api/router.go b/internal/api/router.go index f82dac2f5..f3409ed99 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -9,10 +9,10 @@ import ( pbAuth "github.com/openimsdk/protocol/auth" pbcaptcha "github.com/openimsdk/protocol/captcha" "github.com/openimsdk/protocol/conversation" - pbcrypto "github.com/openimsdk/protocol/crypto" "github.com/openimsdk/protocol/group" "github.com/openimsdk/protocol/msg" "github.com/openimsdk/protocol/relation" + pbcrypto "github.com/openimsdk/protocol/crypto" "github.com/openimsdk/protocol/rtc" "github.com/openimsdk/protocol/third" "github.com/openimsdk/protocol/user" @@ -205,7 +205,6 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co friendRouterGroup.POST("/get_full_friend_user_ids", f.GetFullFriendUserIDs) friendRouterGroup.POST("/get_self_unhandled_apply_count", f.GetSelfUnhandledApplyCount) friendRouterGroup.POST("/get_pinned_friend_ids", f.GetPinnedFriendIDs) - friendRouterGroup.POST("/add_oneway_friend", f.AddOnewayFriend) } g := NewGroupApi(group.NewGroupClient(groupConn)) @@ -335,8 +334,8 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co phoneGroup := r.Group("/phone") phoneGroup.POST("/get_sn_info", phoneSN.GetSNInfo) phoneGroup.POST("/set_sn_info", phoneSN.SetSNInfo) - } - { + } + { rc := NewRtcApi(rtc.NewRtcServiceClient(rtcConn)) rtcGroup := r.Group("/rtc") rtcGroup.POST("/signal_message_assemble", rc.SignalMessageAssemble) diff --git a/internal/msggateway/message_handler.go b/internal/msggateway/message_handler.go index cb1b2ea27..2706540a0 100644 --- a/internal/msggateway/message_handler.go +++ b/internal/msggateway/message_handler.go @@ -188,9 +188,6 @@ func (g *GrpcHandler) SendSignalMessage(ctx context.Context, data *Req) ([]byte, } assembleReq.SignalReq = &signalReq } - - log.ZDebug(ctx, "SendSignalMessage", "assembleReq", assembleReq) - resp, err := g.rtcClient.RtcServiceClient.SignalMessageAssemble(ctx, &assembleReq) if err != nil { log.ZError(ctx, "SendSignalMessage", err, "r", err.Error()) diff --git a/internal/rpc/captcha/captcha.go b/internal/rpc/captcha/captcha.go index 4b5e6c833..206b376e5 100644 --- a/internal/rpc/captcha/captcha.go +++ b/internal/rpc/captcha/captcha.go @@ -2,7 +2,6 @@ package captcha import ( "context" - "encoding/json" "errors" "time" @@ -18,17 +17,10 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" "google.golang.org/grpc" - "github.com/wenlng/go-captcha/v2/click" + "github.com/wenlng/go-captcha/v2/base/option" + "github.com/wenlng/go-captcha/v2/slide" ) -// alphanumChars is the character pool for the click captcha. -// Visually ambiguous characters (I, O, 0, 1, l) are excluded. -var alphanumChars = []string{ - "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", - "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", - "W", "X", "Y", "Z", "2", "3", "4", "5", "6", "7", "8", "9", -} - type Config struct { RpcConfig config.Captcha MongodbConfig config.Mongo @@ -39,14 +31,14 @@ type Config struct { type server struct { pbcaptcha.UnimplementedCaptchaServer conf config.Captcha - capt click.Captcha + capt slide.Captcha collection *mongo.Collection } -// captchaDoc is the MongoDB document that stores the verification answer. type captchaDoc struct { CaptchaID string `bson:"captcha_id"` - DotsJSON string `bson:"dots_json"` // JSON-encoded map[int]*click.Dot (answer dots) + X int `bson:"x"` + Y int `bson:"y"` ExpiredAt time.Time `bson:"expired_at"` CreateTime time.Time `bson:"create_time"` VerifyTime time.Time `bson:"verify_time,omitempty"` @@ -74,15 +66,17 @@ func Start(ctx context.Context, cfg *Config, _ discovery.SvcDiscoveryRegistry, g return err } - capt, err := buildClickCaptcha() + resources, err := loadResources() if err != nil { - log.ZError(ctx, "captcha build click captcha failed", err) + log.ZError(ctx, "captcha load resources failed", err) return err } + builder := slide.NewBuilder() + builder.SetResources(resources...) s := &server{ conf: cfg.RpcConfig, - capt: capt, + capt: builder.Make(), collection: collection, } if s.conf.ExpireSeconds <= 0 { @@ -101,31 +95,24 @@ func (s *server) GenerateCaptcha(ctx context.Context, _ *pbcaptcha.GenerateCaptc log.ZError(ctx, "captcha generate failed", err) return nil, err } - - dots := captData.GetData() // answer dots: map[int]*click.Dot - masterImage, err := captData.GetMasterImage().ToBase64DataWithQuality(0) + block := captData.GetData() + masterImage, err := captData.GetMasterImage().ToBase64DataWithQuality(option.QualityNone) if err != nil { log.ZError(ctx, "captcha encode master image failed", err) return nil, err } - thumbImage, err := captData.GetThumbImage().ToBase64Data() + tileImage, err := captData.GetTileImage().ToBase64Data() if err != nil { - log.ZError(ctx, "captcha encode thumb image failed", err) + log.ZError(ctx, "captcha encode tile image failed", err) return nil, err } - - dotsJSON, err := json.Marshal(dots) - if err != nil { - log.ZError(ctx, "captcha marshal dots failed", err) - return nil, err - } - id := uuid.NewString() now := time.Now() expiredAt := now.Add(time.Duration(s.conf.ExpireSeconds) * time.Second) _, err = s.collection.InsertOne(ctx, captchaDoc{ CaptchaID: id, - DotsJSON: string(dotsJSON), + X: block.X, + Y: block.Y, ExpiredAt: expiredAt, CreateTime: now, }) @@ -133,26 +120,26 @@ func (s *server) GenerateCaptcha(ctx context.Context, _ *pbcaptcha.GenerateCaptc log.ZError(ctx, "captcha insert mongodb failed", err, "captchaID", id) return nil, err } - - log.ZDebug(ctx, "captcha generated", "captchaID", id, "dotCount", len(dots), "expireAt", expiredAt.Unix()) return &pbcaptcha.GenerateCaptchaResp{ CaptchaID: id, MasterImage: masterImage, - ThumbImage: thumbImage, + TileImage: tileImage, + TileY: int32(block.DY), ExpireAt: expiredAt.Unix(), }, nil } func (s *server) VerifyCaptcha(ctx context.Context, req *pbcaptcha.VerifyCaptchaReq) (*pbcaptcha.VerifyCaptchaResp, error) { - log.ZDebug(ctx, "captcha verify request", "captchaID", req.CaptchaID, "clickCount", len(req.ClickPoints)) - now := time.Now() filter := bson.M{ "captcha_id": req.CaptchaID, "verify_time": bson.M{"$exists": false}, } - update := bson.M{"$set": bson.M{"verify_time": now}} - + update := bson.M{ + "$set": bson.M{ + "verify_time": now, + }, + } var doc captchaDoc err := s.collection.FindOneAndUpdate( ctx, @@ -172,41 +159,9 @@ func (s *server) VerifyCaptcha(ctx context.Context, req *pbcaptcha.VerifyCaptcha log.ZWarn(ctx, "captcha expired", nil, "captchaID", req.CaptchaID, "expiredAt", doc.ExpiredAt.Unix()) return nil, servererrs.ErrFileUploadedExpired.WrapMsg("captcha expired", "captchaID", req.CaptchaID) } - - // Unmarshal the stored answer dots. - var answerDots map[int]*click.Dot - if err := json.Unmarshal([]byte(doc.DotsJSON), &answerDots); err != nil { - log.ZError(ctx, "captcha unmarshal dots failed", err, "captchaID", req.CaptchaID) - return nil, servererrs.ErrDatabase.WrapMsg("internal captcha data error") - } - - success := validateClickPoints(req.ClickPoints, answerDots, s.conf.VerifyPadding) + success := slide.Validate(int(req.X), int(req.Y), doc.X, doc.Y, s.conf.VerifyPadding) if !success { - log.ZError(ctx, "captcha validate failed", nil, - "captchaID", req.CaptchaID, - "clickCount", len(req.ClickPoints), - "answerCount", len(answerDots), - ) - } else { - log.ZDebug(ctx, "captcha validate success", "captchaID", req.CaptchaID) + log.ZError(ctx, "captcha validate failed", nil, "captchaID", req.CaptchaID, "x", req.X, "y", req.Y, "docX", doc.X, "docY", doc.Y) } return &pbcaptcha.VerifyCaptchaResp{Success: success}, nil } - -// validateClickPoints checks that each user click point falls within the -// bounding box of the corresponding answer dot (in order). -func validateClickPoints(points []*pbcaptcha.ClickPoint, dots map[int]*click.Dot, padding int) bool { - if len(points) != len(dots) { - return false - } - for i, pt := range points { - dot, ok := dots[i] - if !ok { - return false - } - if !click.Validate(int(pt.X), int(pt.Y), dot.X, dot.Y, dot.Width, dot.Height, padding) { - return false - } - } - return true -} diff --git a/internal/rpc/captcha/embed.go b/internal/rpc/captcha/embed.go index c0c0372bf..9ec2e8d8f 100644 --- a/internal/rpc/captcha/embed.go +++ b/internal/rpc/captcha/embed.go @@ -2,7 +2,12 @@ package captcha import "embed" -// resourceFS embeds background images for the click captcha at compile time. +// resourceFS embeds background images and tile images at compile time. +// Background images come from go-captcha-resources (sourcedata/images/image-{1..5}). +// Tile images come from go-captcha-resources (sourcedata/tiles/tile-{1..4}): +// overlay.png → GraphImage.OverlayImage +// shadow.png → GraphImage.ShadowImage +// mask.png → GraphImage.MaskImage // -//go:embed resources/images/*.jpg +//go:embed resources/images/*.jpg resources/tiles/*/*.png var resourceFS embed.FS diff --git a/internal/rpc/captcha/resources.go b/internal/rpc/captcha/resources.go index d02ce1c29..1264ef1fd 100644 --- a/internal/rpc/captcha/resources.go +++ b/internal/rpc/captcha/resources.go @@ -6,42 +6,24 @@ import ( _ "image/jpeg" _ "image/png" - "github.com/golang/freetype" - "github.com/golang/freetype/truetype" - "github.com/wenlng/go-captcha/v2/base/option" - "github.com/wenlng/go-captcha/v2/click" - "golang.org/x/image/font/gofont/goregular" + "github.com/wenlng/go-captcha/v2/slide" ) -// buildClickCaptcha constructs a click.Captcha instance configured with -// alphanumeric characters, a bundled Go font, and the embedded background images. -func buildClickCaptcha() (click.Captcha, error) { - font, err := loadGoRegularFont() - if err != nil { - return nil, fmt.Errorf("load font: %w", err) - } +// loadResources reads the embedded files and returns slide.Resource options +// ready to be passed to slide.NewBuilder().SetResources(...). +func loadResources() ([]slide.Resource, error) { backgrounds, err := loadBackgrounds() if err != nil { return nil, fmt.Errorf("load captcha backgrounds: %w", err) } - - builder := click.NewBuilder( - click.WithRangeLen(option.RangeVal{Min: 6, Max: 8}), - click.WithRangeVerifyLen(option.RangeVal{Min: 3, Max: 4}), - click.WithRangeSize(option.RangeVal{Min: 26, Max: 34}), - click.WithDisplayShadow(true), - ) - builder.SetResources( - click.WithChars(alphanumChars), - click.WithFonts([]*truetype.Font{font}), - click.WithBackgrounds(backgrounds), - ) - return builder.Make(), nil -} - -// loadGoRegularFont parses the bundled Go Regular TTF font. -func loadGoRegularFont() (*truetype.Font, error) { - return freetype.ParseFont(goregular.TTF) + graphImages, err := loadGraphImages() + if err != nil { + return nil, fmt.Errorf("load captcha graph images: %w", err) + } + return []slide.Resource{ + slide.WithBackgrounds(backgrounds), + slide.WithGraphImages(graphImages), + }, nil } // loadBackgrounds decodes the embedded JPEG background images. @@ -59,6 +41,32 @@ func loadBackgrounds() ([]image.Image, error) { return images, nil } +// loadGraphImages decodes the 4 sets of tile overlay/shadow/mask PNG images. +func loadGraphImages() ([]*slide.GraphImage, error) { + const count = 4 + graphs := make([]*slide.GraphImage, 0, count) + for i := 1; i <= count; i++ { + overlay, err := decodeEmbedImage(fmt.Sprintf("resources/tiles/tile-%d/overlay.png", i)) + if err != nil { + return nil, fmt.Errorf("decode tile-%d overlay: %w", i, err) + } + shadow, err := decodeEmbedImage(fmt.Sprintf("resources/tiles/tile-%d/shadow.png", i)) + if err != nil { + return nil, fmt.Errorf("decode tile-%d shadow: %w", i, err) + } + mask, err := decodeEmbedImage(fmt.Sprintf("resources/tiles/tile-%d/mask.png", i)) + if err != nil { + return nil, fmt.Errorf("decode tile-%d mask: %w", i, err) + } + graphs = append(graphs, &slide.GraphImage{ + OverlayImage: overlay, + ShadowImage: shadow, + MaskImage: mask, + }) + } + return graphs, nil +} + func decodeEmbedImage(path string) (image.Image, error) { f, err := resourceFS.Open(path) if err != nil { diff --git a/internal/rpc/captcha/resources/images/image-1.jpg b/internal/rpc/captcha/resources/images/image-1.jpg index a392ce012..f3be01297 100644 Binary files a/internal/rpc/captcha/resources/images/image-1.jpg and b/internal/rpc/captcha/resources/images/image-1.jpg differ diff --git a/internal/rpc/captcha/resources/images/image-2.jpg b/internal/rpc/captcha/resources/images/image-2.jpg index 36e867881..ca35458f6 100644 Binary files a/internal/rpc/captcha/resources/images/image-2.jpg and b/internal/rpc/captcha/resources/images/image-2.jpg differ diff --git a/internal/rpc/captcha/resources/images/image-3.jpg b/internal/rpc/captcha/resources/images/image-3.jpg index 77cdb21db..104aeb77a 100644 Binary files a/internal/rpc/captcha/resources/images/image-3.jpg and b/internal/rpc/captcha/resources/images/image-3.jpg differ diff --git a/internal/rpc/captcha/resources/images/image-4.jpg b/internal/rpc/captcha/resources/images/image-4.jpg index 5fea0bd09..dc12c2bf4 100644 Binary files a/internal/rpc/captcha/resources/images/image-4.jpg and b/internal/rpc/captcha/resources/images/image-4.jpg differ diff --git a/internal/rpc/captcha/resources/images/image-5.jpg b/internal/rpc/captcha/resources/images/image-5.jpg index 601702436..4a11044a9 100644 Binary files a/internal/rpc/captcha/resources/images/image-5.jpg and b/internal/rpc/captcha/resources/images/image-5.jpg differ diff --git a/internal/rpc/relation/friend.go b/internal/rpc/relation/friend.go index 3ed6cb7a5..81286ac28 100644 --- a/internal/rpc/relation/friend.go +++ b/internal/rpc/relation/friend.go @@ -48,6 +48,9 @@ import ( "google.golang.org/grpc" ) +// keep stable add_source value for one-way friendship even if protocol constants lag behind. +const becomeFriendByOneway int32 = 3 + type friendServer struct { relation.UnimplementedFriendServer db controller.FriendDatabase @@ -690,7 +693,7 @@ func (s *friendServer) AddOnewayFriend(ctx context.Context, req *relation.ApplyT if in1 { return nil, servererrs.ErrRelationshipAlready.WrapMsg("already in friend list") } - if err := s.db.BecomeOnewayFriend(ctx, req.FromUserID, req.ToUserID, constant.BecomeFriendByOneway); err != nil { + if err := s.db.BecomeOnewayFriend(ctx, req.FromUserID, req.ToUserID, becomeFriendByOneway); err != nil { return nil, err } // Notify only A so that A's incremental friend sync is triggered. diff --git a/internal/rpc/rtc/signal.go b/internal/rpc/rtc/signal.go index a3efb70c5..b20e02d36 100644 --- a/internal/rpc/rtc/signal.go +++ b/internal/rpc/rtc/signal.go @@ -48,7 +48,7 @@ func (s *rtcServer) SignalMessageAssemble(ctx context.Context, req *rtc.SignalMe ) switch payload := req.SignalReq.Payload.(type) { case *rtc.SignalReq_Invite: - log.ZDebug(ctx, "SignalMessageAssemble", "payload", payload.Invite) + log.ZInfo(ctx, "SignalMessageAssemble", "payload", payload.Invite) r, err := s.handleInvite(ctx, payload.Invite, req.SignalReq) resp.Payload = &rtc.SignalResp_Invite{Invite: r} respErr = err diff --git a/protocol b/protocol index d78ed4f7b..ed16bd0c4 160000 --- a/protocol +++ b/protocol @@ -1 +1 @@ -Subproject commit d78ed4f7b4563964d1f5250aa80b122ab1ef6b5d +Subproject commit ed16bd0c4049d722e7b605c3f314ee661b9bc4e1 diff --git a/start-config.yml b/start-config.yml index 52afa4cab..d06738143 100644 --- a/start-config.yml +++ b/start-config.yml @@ -13,7 +13,6 @@ serviceBinaries: openim-rpc-msg: 1 openim-rpc-rtc: 1 openim-rpc-third: 1 - openim-rpc-crypto: 1 toolBinaries: - check-free-memory - check-component