Add integration credential verification endpoint to robot API
- Introduce a new POST endpoint for verifying integration credentials at /robots/integrations/verify. - Ensure the endpoint is positioned correctly in the routing to avoid conflicts with existing routes. - Enhance the API's capabilities for integration management by allowing credential verification.
This commit is contained in:
parent
5333302055
commit
efb612734d
2 changed files with 182 additions and 0 deletions
|
|
@ -22,6 +22,9 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
|||
// Activities - Cross-robot activity feed for team (must be before /:id to avoid conflict)
|
||||
group.GET("/activities", ListActivities) // GET /robots/activities - List team activities
|
||||
|
||||
// Integration credential verification (must be before /:id to avoid conflict)
|
||||
group.POST("/integrations/verify", VerifyIntegration) // POST /robots/integrations/verify - Verify integration credentials
|
||||
|
||||
group.GET("/:id", GetRobot) // GET /robots/:id - Get robot details
|
||||
group.PUT("/:id", UpdateRobot) // PUT /robots/:id - Update robot
|
||||
group.DELETE("/:id", DeleteRobot) // DELETE /robots/:id - Delete robot
|
||||
|
|
|
|||
179
openapi/agent/robot/verify.go
Normal file
179
openapi/agent/robot/verify.go
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
package robot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
||||
"github.com/yaoapp/yao/integrations/dingtalk"
|
||||
"github.com/yaoapp/yao/integrations/discord"
|
||||
"github.com/yaoapp/yao/integrations/feishu"
|
||||
"github.com/yaoapp/yao/integrations/telegram"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
)
|
||||
|
||||
// VerifyIntegrationRequest — POST body for credential verification.
|
||||
type VerifyIntegrationRequest struct {
|
||||
Provider string `json:"provider" binding:"required"` // telegram | feishu | dingtalk | discord
|
||||
Config map[string]any `json:"config" binding:"required"`
|
||||
}
|
||||
|
||||
// VerifyIntegrationResponse — returned to the frontend.
|
||||
type VerifyIntegrationResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
Info map[string]any `json:"info,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// VerifyIntegration tests whether the supplied credentials are valid
|
||||
// by making a lightweight API call to the target platform.
|
||||
//
|
||||
// POST /v1/agent/robots/integrations/verify
|
||||
func VerifyIntegration(c *gin.Context) {
|
||||
var req VerifyIntegrationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.RespondWithError(c, response.StatusBadRequest, &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Invalid request body: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var resp VerifyIntegrationResponse
|
||||
|
||||
switch req.Provider {
|
||||
case "telegram":
|
||||
resp = verifyTelegram(ctx, req.Config)
|
||||
case "feishu":
|
||||
resp = verifyFeishu(ctx, req.Config)
|
||||
case "dingtalk":
|
||||
resp = verifyDingtalk(ctx, req.Config)
|
||||
case "discord":
|
||||
resp = verifyDiscord(ctx, req.Config)
|
||||
default:
|
||||
response.RespondWithError(c, response.StatusBadRequest, &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: fmt.Sprintf("unsupported provider: %s", req.Provider),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
response.RespondWithSuccess(c, response.StatusOK, resp)
|
||||
}
|
||||
|
||||
func str(m map[string]any, key string) string {
|
||||
v, _ := m[key].(string)
|
||||
return v
|
||||
}
|
||||
|
||||
func verifyTelegram(ctx context.Context, cfg map[string]any) VerifyIntegrationResponse {
|
||||
token := str(cfg, "bot_token")
|
||||
if token == "" {
|
||||
return VerifyIntegrationResponse{Valid: false, Error: "bot_token is required"}
|
||||
}
|
||||
|
||||
var opts []telegram.BotOption
|
||||
if host := str(cfg, "host"); host != "" {
|
||||
opts = append(opts, telegram.WithAPIBase(host))
|
||||
}
|
||||
|
||||
bot := telegram.NewBot(token, "", opts...)
|
||||
user, err := bot.GetMe(ctx)
|
||||
if err != nil {
|
||||
return VerifyIntegrationResponse{Valid: false, Error: err.Error()}
|
||||
}
|
||||
|
||||
info := map[string]any{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"name": user.FirstName,
|
||||
}
|
||||
if user.LastName != "" {
|
||||
info["name"] = user.FirstName + " " + user.LastName
|
||||
}
|
||||
return VerifyIntegrationResponse{Valid: true, Info: info}
|
||||
}
|
||||
|
||||
func verifyFeishu(ctx context.Context, cfg map[string]any) VerifyIntegrationResponse {
|
||||
appID := str(cfg, "app_id")
|
||||
appSecret := str(cfg, "app_secret")
|
||||
if appID == "" || appSecret == "" {
|
||||
return VerifyIntegrationResponse{Valid: false, Error: "app_id and app_secret are required"}
|
||||
}
|
||||
|
||||
bot := feishu.NewBot(appID, appSecret)
|
||||
|
||||
resp, err := bot.Client().GetTenantAccessTokenBySelfBuiltApp(ctx,
|
||||
&larkcore.SelfBuiltTenantAccessTokenReq{AppID: appID, AppSecret: appSecret})
|
||||
if err != nil {
|
||||
return VerifyIntegrationResponse{Valid: false, Error: err.Error()}
|
||||
}
|
||||
if resp == nil || !resp.Success() {
|
||||
msg := "failed to obtain tenant access token"
|
||||
if resp != nil {
|
||||
msg = fmt.Sprintf("code=%d msg=%s", resp.Code, resp.Msg)
|
||||
}
|
||||
return VerifyIntegrationResponse{Valid: false, Error: msg}
|
||||
}
|
||||
|
||||
return VerifyIntegrationResponse{
|
||||
Valid: true,
|
||||
Info: map[string]any{
|
||||
"app_id": appID,
|
||||
"status": "credentials_valid",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func verifyDingtalk(ctx context.Context, cfg map[string]any) VerifyIntegrationResponse {
|
||||
clientID := str(cfg, "client_id")
|
||||
clientSecret := str(cfg, "client_secret")
|
||||
if clientID == "" || clientSecret == "" {
|
||||
return VerifyIntegrationResponse{Valid: false, Error: "client_id and client_secret are required"}
|
||||
}
|
||||
|
||||
bot := dingtalk.NewBot(clientID, clientSecret)
|
||||
if err := bot.GetBotInfo(ctx); err != nil {
|
||||
return VerifyIntegrationResponse{Valid: false, Error: err.Error()}
|
||||
}
|
||||
|
||||
return VerifyIntegrationResponse{
|
||||
Valid: true,
|
||||
Info: map[string]any{
|
||||
"client_id": clientID,
|
||||
"status": "credentials_valid",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func verifyDiscord(ctx context.Context, cfg map[string]any) VerifyIntegrationResponse {
|
||||
botToken := str(cfg, "bot_token")
|
||||
if botToken == "" {
|
||||
return VerifyIntegrationResponse{Valid: false, Error: "bot_token is required"}
|
||||
}
|
||||
|
||||
appID := str(cfg, "app_id")
|
||||
bot, err := discord.NewBot(botToken, appID)
|
||||
if err != nil {
|
||||
return VerifyIntegrationResponse{Valid: false, Error: err.Error()}
|
||||
}
|
||||
|
||||
user, err := bot.BotUser()
|
||||
if err != nil {
|
||||
return VerifyIntegrationResponse{Valid: false, Error: err.Error()}
|
||||
}
|
||||
|
||||
return VerifyIntegrationResponse{
|
||||
Valid: true,
|
||||
Info: map[string]any{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"name": user.GlobalName,
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue