钉钉企业内部机器人开发——绑定群聊信息到机器人

开发机器人的过程中,需要将用户添加的机器人存储在数据库中,对于企业内部机器人,官方只提供一个token,其他的机器人信息都没有提供。 对于自定义webhook机器人,还多了一个secret,其实,一个机器人应该有如下的信息

type DingRobot struct {
    RobotId            string         `gorm:"primaryKey;foreignKey:RobotId" json:"robot_id"` //机器人的token
    Deleted            gorm.DeletedAt `json:"deleted"` //软删除字段
    Type               string         `json:"type"` //机器人类型,1为企业内部机器人,2为自定义webhook机器人
    TypeDetail         string         `json:"type_detail"`  //具体机器人类型
    ChatBotUserId      string         `json:"chat_bot_user_id"` //加密的机器人id,该字段无用
    Secret             string         `json:"secret"` //如果是自定义成机器人, 则存在此字段
    DingUsers          []DingUser     `json:"ding_users" gorm:"many2many:user_robot"` //机器人@多个人,一个人可以被多个机器人@
    ChatId             string         `json:"chat_id"` //机器人所在的群聊chatId
    OpenConversationID string         `json:"open_conversation_id"`//机器人所在的群聊openConversationID
    Tasks              []Task         `gorm:"foreignKey:RobotId;references:RobotId"` //机器人拥有多个任务
    Name               string         `json:"name"` //机器人的名称
    DingToken          `json:"ding_token" gorm:"-"`
}

其中,DingToken字段中存储的是token,也就是访问钉钉接口的凭证,Task字段是钉钉机器人拥有的定时任务,是我自己封装的,还有一个字段是DingUsers ,也就是说一个钉钉里面存了好多个用户,这些用户就是群成员,存这些用户的目的是为了能够让机器人@群成员。

其中,RobotId就是token,是唯一的,可以当做主键。 而 ChatBotUserId DingUsers ChatId OpenConversationID Name 如何获取获取呢?

上面的问题只针对于企业内部机器人,如果是自定义机器人,则不需存这些字段。

解决方案:

首先,我们知道RobotId,也就是机器人的token,此token是调用机器人的关键信息,我们先把RobotId存储起来

然后我们使用钉钉的调试器

https://open-dev.dingtalk.com/apiExplorer?spm=ding_open_doc.document.0.0.20bf4063FEGqWg#/jsapi?api=biz.chat.chooseConversationByCorpId

输入cropId之后,我们可以在手机上面选择群聊信息,然后可以获取到chatId(chatId必须通过手机扫描二维码授权)和Title(群聊名称,可以作为机器人的名称),然后我们拿着chatId可以获取到openconversationId,然后通过openConverstaionId可以到该群的所有群成员信息(用来实现机器人@群成员)。

上面是大体的思路,经过上面一番操作后,就可以把原来只有一个robotId机企业内部机器人给绑定上其所在的群成员信息和群聊基本信息。

但是,如果想要让用户使用,首先用户肯定不能手动打开钉钉调试器,其次是用户也不知道企业的cropId,所以我们需要使用程序,让该功能简单化,理想的情况是,用户输入机器人RobotId,然后加载二维码,然后用户使用手机选择机器人所在的群聊,之后机器人就和群聊信息已经该群的群成员绑定在一起了。

难点一:

如何让用户扫描二维码?此二维码实时更新,而且被隐藏在一个canvas中,无法获取到。 解决办法是,我们使用chromedp来模拟浏览器操作,直接把cropId放在程序,自动输入即可,至于二维码,直接使用chromedp进行截图,然后存储在数据库中,然后渲染给前端,等待用户扫描即可

难点二:

如何获取所有群成员,钉钉开放的有接口,我们通过二维码扫码获取chatId,然后获取openconversationId,然后在钉钉开放的小程序上面添加一个酷应用,然后把酷应用添加到群聊中,才可以获取到群成员信息,不然,就会出现系统错误(该问题是我请教钉钉技术支持解决的)

部分源代码展示

//获取二维码buf,chatId, title
func (u *DingUser) GetQRCode(c *gin.Context) (buf []byte, chatId, title string, err error) {
    d := data{}
    opts := append(
        chromedp.DefaultExecAllocatorOptions[:],
        chromedp.NoDefaultBrowserCheck, //不检查默认浏览器
        chromedp.Flag("headless", false),
        chromedp.Flag("blink-settings", "imagesEnabled=true"), //开启图像界面,重点是开启这个
        chromedp.Flag("ignore-certificate-errors", true),      //忽略错误
        chromedp.Flag("disable-web-security", true),           //禁用网络安全标志
        chromedp.Flag("disable-extensions", true),             //开启插件支持
        chromedp.Flag("disable-default-apps", true),
        chromedp.NoFirstRun, //设置网站不是首次运行
        chromedp.WindowSize(1921, 1024),
        chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36"), //设置UserAgent
    )
    allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
    //defer cancel()
    print(cancel)

    // 创建上下文实例
    ctx, cancel := chromedp.NewContext(
        allocCtx,
        chromedp.WithLogf(log.Printf),
    )
    //defer cancel()

    // 创建超时上下文
    ctx, cancel = context.WithTimeout(ctx, 10*time.Minute)
    //defer cancel()

    // navigate to a page, wait for an element, click

    // capture screenshot of an element

    // capture entire browser viewport, returning png with quality=90
    var html string

    if err := chromedp.Run(ctx,
        //打开网页
        chromedp.Navigate(`https://open-dev.dingtalk.com/apiExplorer?spm=ding_open_doc.document.0.0.20bf4063FEGqWg#/jsapi?api=biz.chat.chooseConversationByCorpId`),
        //定位登录按钮
        chromedp.Click(`document.querySelector(".ant-btn.ant-btn-primary")`, chromedp.ByJSPath),
        //等二维码出现
        chromedp.WaitVisible(`document.querySelector(".ant-modal")`, chromedp.ByJSPath),
        //截图
        chromedp.ActionFunc(func(ctx context.Context) error {
            // get layout metrics
            _, _, _, _, _, contentSize, err := page.GetLayoutMetrics().Do(ctx)
            if err != nil {
                return err
            }

            width, height := int64(math.Ceil(contentSize.Width)), int64(math.Ceil(contentSize.Height))

            // force viewport emulation
            err = emulation.SetDeviceMetricsOverride(width, height, 1, false).
                WithScreenOrientation(&emulation.ScreenOrientation{
                    Type:  emulation.OrientationTypePortraitPrimary,
                    Angle: 0,
                }).
                Do(ctx)
            if err != nil {
                return err
            }

            // capture screenshot
            buf, err = page.CaptureScreenshot().
                WithQuality(90).
                WithClip(&page.Viewport{
                    X:      contentSize.X,
                    Y:      contentSize.Y,
                    Width:  contentSize.Width,
                    Height: contentSize.Height,
                    Scale:  1,
                }).Do(ctx)
            username, _ := c.Get(global.CtxUserNameKey)
            err = ioutil.WriteFile(fmt.Sprintf("./Screenshot_%s.png", username), buf, 0644)
            if err != nil {
                zap.L().Error("二维码写入失败", zap.Error(err))
            }
            return nil
        }),
        //等待用户扫码连接成功
        chromedp.WaitVisible(`document.querySelector(".connect-info")`, chromedp.ByJSPath),
        //chromedp.SendKeys(`document.querySelector("#corpId")`, "caonima",chromedp.ByJSPath),
        //设置输入框中的值为空
        chromedp.SetValue(`document.querySelector("#corpId")`, "", chromedp.ByJSPath),
        //chromedp.Click(`document.querySelector(".ant-btn.ant-btn-primary")`, chromedp.ByJSPath),
        //chromedp.Clear(`#corpId`,chromedp.ByID),
        //输入正确的值
        chromedp.SendKeys(`document.querySelector("#corpId")`, "输入自己企业的cropId", chromedp.ByJSPath),
        //点击发起调用按钮
        chromedp.Click(`document.querySelector(".ant-btn.ant-btn-primary")`, chromedp.ByJSPath),

        chromedp.WaitVisible(`document.querySelector("#dingapp > div > div > div.api-explorer-wrap > div.api-info > div > div.ant-tabs-content.ant-tabs-content-animated.ant-tabs-top-content > div.ant-tabs-tabpane.ant-tabs-tabpane-active > div.debug-result > div.code-mirror > div.code-content > div > div > div.CodeMirror-scroll > div.CodeMirror-sizer > div > div > div > div.CodeMirror-code > div:nth-child(2) > pre > span > span.cm-tab")`, chromedp.ByJSPath),
        //自定义函数进行爬虫
        chromedp.ActionFunc(func(ctx context.Context) error {
            //b := chromedp.WaitEnabled(`document.querySelector("#dingapp > div > div > div.api-explorer-wrap > div.api-info > div > div.ant-tabs-content.ant-tabs-content-animated.ant-tabs-top-content > div.ant-tabs-tabpane.ant-tabs-tabpane-active > div.debug-result > div.code-mirror > div.code-content > div > div > div.CodeMirror-scroll > div.CodeMirror-sizer > div > div > div > div.CodeMirror-code > div > pre")`, chromedp.ByJSPath)
            //b.Do(ctx)
            a := chromedp.OuterHTML(`document.querySelector("body")`, &html, chromedp.ByJSPath)
            a.Do(ctx)
            dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
            if err != nil {
                fmt.Println("123", err.Error())
                return err
            }
            var data string
            dom.Find("#dingapp > div > div > div.api-explorer-wrap > div.api-info > div > div.ant-tabs-content.ant-tabs-content-animated.ant-tabs-top-content > div.ant-tabs-tabpane.ant-tabs-tabpane-active > div.debug-result > div.code-mirror > div.code-content > div > div > div.CodeMirror-scroll > div.CodeMirror-sizer > div > div > div > div.CodeMirror-code > div > pre").Each(func(i int, selection *goquery.Selection) {
                data = data + selection.Text()
                selection.Next()
            })
            data = strings.ReplaceAll(data, " ", "")
            data = strings.ReplaceAll(data, "\n", "")
            reader := strings.NewReader(data)
            bytearr, err := ioutil.ReadAll(reader)

            err1 := json.Unmarshal(bytearr, &d)
            if err1 != nil {

            }
            return nil
        }),
    ); err != nil {
        zap.L().Error("使用chromedp失败")
        return nil, "", "", err
    }
    if &d == nil {
        return nil, "", "", err
    }
    return buf, d.Result.ChatId, d.Result.Title, err
}

把图片返回前端代码

User.GET("showQRCode", func(c *gin.Context) {
            username, _ := c.Get(global.CtxUserNameKey)
            c.File(fmt.Sprintf("Screenshot_%s.png", username))
        })

获取openConversationId

type DingGroup struct {
    OpenConversationID string
    ChatID             string
    Name               string
    Token              DingToken
}
func (g *DingGroup) GetOpenConversationID() string {
    client, _err := createClient()
    if _err != nil {
        return g.OpenConversationID
    }

    chatIdToOpenConversationIdHeaders := &dingtalkim_1_0.ChatIdToOpenConversationIdHeaders{}
    chatIdToOpenConversationIdHeaders.XAcsDingtalkAccessToken = tea.String(g.Token.Token)
    tryErr := func() (_e error) {
        defer func() {
            if r := tea.Recover(recover()); r != nil {
                _e = r
            }
        }()
        result, _err := client.ChatIdToOpenConversationIdWithOptions(tea.String(g.ChatID), chatIdToOpenConversationIdHeaders, &util.RuntimeOptions{})
        if _err != nil {
            return _err
        }
        g.OpenConversationID = *(result.Body.OpenConversationId)
        return nil
    }()

    if tryErr != nil {
        var err = &tea.SDKError{}
        if _t, ok := tryErr.(*tea.SDKError); ok {
            err = _t
        } else {
            err.Message = tea.String(tryErr.Error())
        }
        if !tea.BoolValue(util.Empty(err.Code)) && !tea.BoolValue(util.Empty(err.Message)) {
            // err 中含有 code 和 message 属性,可帮助开发定位问题
        }

    }
    return g.OpenConversationID
}

获取机器人所在的群成员

type DingRobot struct {
    RobotId            string         `gorm:"primaryKey;foreignKey:RobotId" json:"robot_id"` //机器人的token
    Deleted            gorm.DeletedAt `json:"deleted"`                                       //软删除字段
    Type               string         `json:"type"`                                          //机器人类型,1为企业内部机器人,2为自定义webhook机器人
    TypeDetail         string         `json:"type_detail"`                                   //具体机器人类型
    ChatBotUserId      string         `json:"chat_bot_user_id"`                              //加密的机器人id,该字段无用
    Secret             string         `json:"secret"`                                        //如果是自定义成机器人, 则存在此字段
    DingUserID         string         `json:"ding_user_id"`                                  // 机器人所属用户id
    UserName           string         `json:"user_name"`                                     //机器人所属用户名
    DingUsers          []DingUser     `json:"ding_users" gorm:"many2many:user_robot"`        //机器人@多个人,一个人可以被多个机器人@
    ChatId             string         `json:"chat_id"`                                       //机器人所在的群聊chatId
    OpenConversationID string         `json:"open_conversation_id"`                          //机器人所在的群聊openConversationID
    Tasks              []Task         `gorm:"foreignKey:RobotId;references:RobotId"`         //机器人拥有多个任务
    Name               string         `json:"name"`                                          //机器人的名称
    DingToken          `json:"ding_token" gorm:"-"`
}
//获取机器人所在的群聊的userIdList ,前提是获取到OpenConversationId,获取到OpenConverstaionId的前提是获取到二维码

func (r *DingRobot) GetGroupUserIds() (userIds []string, _err error) {
    //所需参数access_token, OpenConversationId string
    olduserIds := []*string{}
    client, _err := createClient()
    if _err != nil {
        return
    }

    batchQueryGroupMemberHeaders := &dingtalkim_1_0.BatchQueryGroupMemberHeaders{}
    batchQueryGroupMemberHeaders.XAcsDingtalkAccessToken = tea.String(r.DingToken.Token)
    batchQueryGroupMemberRequest := &dingtalkim_1_0.BatchQueryGroupMemberRequest{
        OpenConversationId: tea.String(r.OpenConversationID),
        CoolAppCode:        tea.String("小程序下面的酷应用编码"),
        MaxResults:         tea.Int64(300),
        NextToken:          tea.String("XXXXX"),
    }
    tryErr := func() (_e error) {
        defer func() {
            if r := tea.Recover(recover()); r != nil {
                _e = r
            }
        }()
        result, _err := client.BatchQueryGroupMemberWithOptions(batchQueryGroupMemberRequest, batchQueryGroupMemberHeaders, &util.RuntimeOptions{})
        if _err != nil {
            return _err
        }
        olduserIds = result.Body.MemberUserIds
        return
    }()

    if tryErr != nil {
        var err = &tea.SDKError{}
        if _t, ok := tryErr.(*tea.SDKError); ok {
            err = _t
        } else {
            err.Message = tea.String(tryErr.Error())
        }
        if !tea.BoolValue(util.Empty(err.Code)) && !tea.BoolValue(util.Empty(err.Message)) {
            // err 中含有 code 和 message 属性,可帮助开发定位问题
        }

    }
    userIds = make([]string, len(olduserIds))
    for i, id := range olduserIds {
        userIds[i] = *id
    }
    return
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值