captcha slide

This commit is contained in:
hawklin2017 2026-04-26 18:06:38 +08:00
parent 44c11c4521
commit 722c657bc8
16 changed files with 104 additions and 115 deletions

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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())

View File

@ -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
}

View File

@ -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

View File

@ -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 {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -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.

View File

@ -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

@ -1 +1 @@
Subproject commit d78ed4f7b4563964d1f5250aa80b122ab1ef6b5d
Subproject commit ed16bd0c4049d722e7b605c3f314ee661b9bc4e1

View File

@ -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