mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
301 lines
9.4 KiB
Go
301 lines
9.4 KiB
Go
package feishu
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
lark "github.com/larksuite/oapi-sdk-go/v3"
|
|
larkcontact "github.com/larksuite/oapi-sdk-go/v3/service/contact/v3"
|
|
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
|
|
|
|
"github.com/memohai/memoh/internal/channel"
|
|
)
|
|
|
|
const (
|
|
defaultDirectoryPageSize = 20
|
|
maxDirectoryPageSize = 200
|
|
)
|
|
|
|
func directoryLimit(n int) int {
|
|
if n <= 0 {
|
|
return defaultDirectoryPageSize
|
|
}
|
|
if n > maxDirectoryPageSize {
|
|
return maxDirectoryPageSize
|
|
}
|
|
return n
|
|
}
|
|
|
|
// ListPeers lists users (peers) from Feishu contact, optionally filtered by query.
|
|
func (a *FeishuAdapter) ListPeers(ctx context.Context, cfg channel.ChannelConfig, query channel.DirectoryQuery) ([]channel.DirectoryEntry, error) {
|
|
feishuCfg, err := parseConfig(cfg.Credentials)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
client := lark.NewClient(feishuCfg.AppID, feishuCfg.AppSecret, lark.WithOpenBaseUrl(feishuCfg.openBaseURL()))
|
|
pageSize := directoryLimit(query.Limit)
|
|
req := larkcontact.NewListUserReqBuilder().
|
|
UserIdType(larkcontact.UserIdTypeOpenId).
|
|
DepartmentIdType(larkcontact.DepartmentIdTypeOpenDepartmentId).
|
|
DepartmentId("0").
|
|
PageSize(pageSize).
|
|
Build()
|
|
resp, err := client.Contact.User.List(ctx, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("feishu list users: %w", err)
|
|
}
|
|
if !resp.Success() {
|
|
return nil, fmt.Errorf("feishu list users: code=%d msg=%s", resp.Code, resp.Msg)
|
|
}
|
|
entries := make([]channel.DirectoryEntry, 0, len(resp.Data.Items))
|
|
for _, u := range resp.Data.Items {
|
|
e := feishuUserToEntry(u)
|
|
if query.Query != "" && !strings.Contains(strings.ToLower(e.Name+e.Handle), strings.ToLower(query.Query)) {
|
|
continue
|
|
}
|
|
entries = append(entries, e)
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
// ListGroups lists chat groups from Feishu IM, optionally filtered by query.
|
|
func (a *FeishuAdapter) ListGroups(ctx context.Context, cfg channel.ChannelConfig, query channel.DirectoryQuery) ([]channel.DirectoryEntry, error) {
|
|
feishuCfg, err := parseConfig(cfg.Credentials)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
client := lark.NewClient(feishuCfg.AppID, feishuCfg.AppSecret, lark.WithOpenBaseUrl(feishuCfg.openBaseURL()))
|
|
pageSize := directoryLimit(query.Limit)
|
|
var items []*larkim.ListChat
|
|
if strings.TrimSpace(query.Query) != "" {
|
|
req := larkim.NewSearchChatReqBuilder().
|
|
UserIdType("open_id").
|
|
Query(strings.TrimSpace(query.Query)).
|
|
PageSize(pageSize).
|
|
Build()
|
|
resp, err := client.Im.Chat.Search(ctx, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("feishu search chats: %w", err)
|
|
}
|
|
if !resp.Success() {
|
|
return nil, fmt.Errorf("feishu search chats: code=%d msg=%s", resp.Code, resp.Msg)
|
|
}
|
|
items = resp.Data.Items
|
|
} else {
|
|
req := larkim.NewListChatReqBuilder().
|
|
UserIdType("open_id").
|
|
PageSize(pageSize).
|
|
Build()
|
|
resp, err := client.Im.Chat.List(ctx, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("feishu list chats: %w", err)
|
|
}
|
|
if !resp.Success() {
|
|
return nil, fmt.Errorf("feishu list chats: code=%d msg=%s", resp.Code, resp.Msg)
|
|
}
|
|
items = resp.Data.Items
|
|
}
|
|
entries := make([]channel.DirectoryEntry, 0, len(items))
|
|
for _, c := range items {
|
|
entries = append(entries, feishuChatToEntry(c))
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
// ListGroupMembers lists members of a Feishu chat group.
|
|
func (a *FeishuAdapter) ListGroupMembers(ctx context.Context, cfg channel.ChannelConfig, groupID string, query channel.DirectoryQuery) ([]channel.DirectoryEntry, error) {
|
|
feishuCfg, err := parseConfig(cfg.Credentials)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
chatID := strings.TrimSpace(groupID)
|
|
if strings.HasPrefix(chatID, "chat_id:") {
|
|
chatID = strings.TrimPrefix(chatID, "chat_id:")
|
|
}
|
|
if chatID == "" {
|
|
return nil, fmt.Errorf("feishu list group members: empty group id")
|
|
}
|
|
client := lark.NewClient(feishuCfg.AppID, feishuCfg.AppSecret, lark.WithOpenBaseUrl(feishuCfg.openBaseURL()))
|
|
pageSize := directoryLimit(query.Limit)
|
|
req := larkim.NewGetChatMembersReqBuilder().
|
|
ChatId(chatID).
|
|
MemberIdType("open_id").
|
|
PageSize(pageSize).
|
|
Build()
|
|
resp, err := client.Im.ChatMembers.Get(ctx, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("feishu get chat members: %w", err)
|
|
}
|
|
if !resp.Success() {
|
|
return nil, fmt.Errorf("feishu get chat members: code=%d msg=%s", resp.Code, resp.Msg)
|
|
}
|
|
entries := make([]channel.DirectoryEntry, 0, len(resp.Data.Items))
|
|
for _, m := range resp.Data.Items {
|
|
e := feishuMemberToEntry(m)
|
|
if query.Query != "" && !strings.Contains(strings.ToLower(e.Name+e.Handle), strings.ToLower(query.Query)) {
|
|
continue
|
|
}
|
|
entries = append(entries, e)
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
// ResolveEntry resolves an input string to a user or group DirectoryEntry.
|
|
func (a *FeishuAdapter) ResolveEntry(ctx context.Context, cfg channel.ChannelConfig, input string, kind channel.DirectoryEntryKind) (channel.DirectoryEntry, error) {
|
|
feishuCfg, err := parseConfig(cfg.Credentials)
|
|
if err != nil {
|
|
return channel.DirectoryEntry{}, err
|
|
}
|
|
client := lark.NewClient(feishuCfg.AppID, feishuCfg.AppSecret, lark.WithOpenBaseUrl(feishuCfg.openBaseURL()))
|
|
input = strings.TrimSpace(input)
|
|
switch kind {
|
|
case channel.DirectoryEntryUser:
|
|
return a.resolveUser(ctx, client, input)
|
|
case channel.DirectoryEntryGroup:
|
|
return a.resolveGroup(ctx, client, input)
|
|
default:
|
|
return channel.DirectoryEntry{}, fmt.Errorf("feishu resolve entry: unsupported kind %q", kind)
|
|
}
|
|
}
|
|
|
|
func (a *FeishuAdapter) resolveUser(ctx context.Context, client *lark.Client, input string) (channel.DirectoryEntry, error) {
|
|
userID, userIDType := parseFeishuUserInput(input)
|
|
if userID == "" {
|
|
return channel.DirectoryEntry{}, fmt.Errorf("feishu resolve entry user: invalid input %q", input)
|
|
}
|
|
req := larkcontact.NewGetUserReqBuilder().
|
|
UserId(userID).
|
|
UserIdType(userIDType).
|
|
Build()
|
|
resp, err := client.Contact.User.Get(ctx, req)
|
|
if err != nil {
|
|
return channel.DirectoryEntry{}, fmt.Errorf("feishu get user: %w", err)
|
|
}
|
|
if !resp.Success() {
|
|
return channel.DirectoryEntry{}, fmt.Errorf("feishu get user: code=%d msg=%s", resp.Code, resp.Msg)
|
|
}
|
|
if resp.Data == nil || resp.Data.User == nil {
|
|
return channel.DirectoryEntry{}, fmt.Errorf("feishu get user: empty response")
|
|
}
|
|
return feishuUserToEntry(resp.Data.User), nil
|
|
}
|
|
|
|
func (a *FeishuAdapter) resolveGroup(ctx context.Context, client *lark.Client, input string) (channel.DirectoryEntry, error) {
|
|
chatID := strings.TrimSpace(input)
|
|
if strings.HasPrefix(chatID, "chat_id:") {
|
|
chatID = strings.TrimPrefix(chatID, "chat_id:")
|
|
}
|
|
if chatID == "" {
|
|
return channel.DirectoryEntry{}, fmt.Errorf("feishu resolve entry group: invalid input %q", input)
|
|
}
|
|
req := larkim.NewGetChatReqBuilder().
|
|
ChatId(chatID).
|
|
UserIdType("open_id").
|
|
Build()
|
|
resp, err := client.Im.Chat.Get(ctx, req)
|
|
if err != nil {
|
|
return channel.DirectoryEntry{}, fmt.Errorf("feishu get chat: %w", err)
|
|
}
|
|
if !resp.Success() {
|
|
return channel.DirectoryEntry{}, fmt.Errorf("feishu get chat: code=%d msg=%s", resp.Code, resp.Msg)
|
|
}
|
|
return channel.DirectoryEntry{
|
|
Kind: channel.DirectoryEntryGroup,
|
|
ID: "chat_id:" + chatID,
|
|
Name: ptrStr(resp.Data.Name),
|
|
AvatarURL: ptrStr(resp.Data.Avatar),
|
|
Metadata: map[string]any{"chat_id": chatID},
|
|
}, nil
|
|
}
|
|
|
|
func parseFeishuUserInput(raw string) (userID, userIDType string) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return "", ""
|
|
}
|
|
if strings.HasPrefix(raw, "open_id:") {
|
|
return strings.TrimSpace(strings.TrimPrefix(raw, "open_id:")), larkcontact.UserIdTypeOpenId
|
|
}
|
|
if strings.HasPrefix(raw, "user_id:") {
|
|
return strings.TrimSpace(strings.TrimPrefix(raw, "user_id:")), larkcontact.UserIdTypeUserId
|
|
}
|
|
if strings.HasPrefix(raw, "ou_") {
|
|
return raw, larkcontact.UserIdTypeOpenId
|
|
}
|
|
if strings.HasPrefix(raw, "u_") || strings.HasPrefix(raw, "u-") {
|
|
return raw, larkcontact.UserIdTypeUserId
|
|
}
|
|
// For raw IDs without explicit prefix, default to user_id. In practice
|
|
// open_id is usually "ou_*", while bare IDs are commonly user_id.
|
|
return raw, larkcontact.UserIdTypeUserId
|
|
}
|
|
|
|
func feishuUserToEntry(u *larkcontact.User) channel.DirectoryEntry {
|
|
openID := ptrStr(u.OpenId)
|
|
userID := ptrStr(u.UserId)
|
|
id := "open_id:" + openID
|
|
if openID == "" && userID != "" {
|
|
id = "user_id:" + userID
|
|
}
|
|
meta := make(map[string]any)
|
|
if u.OpenId != nil {
|
|
meta["open_id"] = *u.OpenId
|
|
}
|
|
if u.UserId != nil {
|
|
meta["user_id"] = *u.UserId
|
|
}
|
|
return channel.DirectoryEntry{
|
|
Kind: channel.DirectoryEntryUser,
|
|
ID: id,
|
|
Name: ptrStr(u.Name),
|
|
Handle: ptrStr(u.Nickname),
|
|
AvatarURL: feishuAvatarURL(u.Avatar),
|
|
Metadata: meta,
|
|
}
|
|
}
|
|
|
|
func feishuChatToEntry(c *larkim.ListChat) channel.DirectoryEntry {
|
|
chatID := ptrStr(c.ChatId)
|
|
meta := map[string]any{"chat_id": chatID}
|
|
return channel.DirectoryEntry{
|
|
Kind: channel.DirectoryEntryGroup,
|
|
ID: "chat_id:" + chatID,
|
|
Name: ptrStr(c.Name),
|
|
AvatarURL: ptrStr(c.Avatar),
|
|
Metadata: meta,
|
|
}
|
|
}
|
|
|
|
func feishuMemberToEntry(m *larkim.ListMember) channel.DirectoryEntry {
|
|
id := ptrStr(m.MemberId)
|
|
meta := make(map[string]any)
|
|
if m.MemberIdType != nil {
|
|
meta["member_id_type"] = *m.MemberIdType
|
|
}
|
|
prefix := "open_id:"
|
|
if m.MemberIdType != nil && *m.MemberIdType == "user_id" {
|
|
prefix = "user_id:"
|
|
}
|
|
return channel.DirectoryEntry{
|
|
Kind: channel.DirectoryEntryUser,
|
|
ID: prefix + id,
|
|
Name: ptrStr(m.Name),
|
|
Metadata: meta,
|
|
}
|
|
}
|
|
|
|
func ptrStr(s *string) string {
|
|
if s == nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(*s)
|
|
}
|
|
|
|
func feishuAvatarURL(avatar *larkcontact.AvatarInfo) string {
|
|
if avatar == nil || avatar.Avatar72 == nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(*avatar.Avatar72)
|
|
}
|