mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-15 00:04:58 +02:00
Adds a DingTalk (钉钉) bot integration on the bring-your-own-app model: a workspace admin creates their own Stream-mode robot and pastes its AppKey / AppSecret, so no public webhook or OAuth redirect is required. Each agent gets its own bot identity, so several agents can be distinct, separately @-mentionable contacts in one DingTalk organization. Supports DMs, @-mentions in groups, inbound images, /issue quick-create, and /new. Built on the shared channel engine (ForceFresh/BareFresh, MediaResolver / MediaRef and the intent ledger) rather than a private implementation. Off unless MULTICA_DINGTALK_SECRET_KEY is set. Docs in en/zh/ja/ko. Closes #4791. Community-maintained: @yyclaw is the code owner for server/internal/integrations/dingtalk/.
165 lines
5.6 KiB
Go
165 lines
5.6 KiB
Go
package dingtalk
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
|
|
"github.com/multica-ai/multica/server/internal/util"
|
|
"github.com/multica-ai/multica/server/internal/util/secretbox"
|
|
db "github.com/multica-ai/multica/server/pkg/db/generated"
|
|
)
|
|
|
|
func testBox(t *testing.T) *secretbox.Box {
|
|
t.Helper()
|
|
key := make([]byte, secretbox.KeySize)
|
|
for i := range key {
|
|
key[i] = byte(i + 1)
|
|
}
|
|
box, err := secretbox.New(key)
|
|
if err != nil {
|
|
t.Fatalf("secretbox.New: %v", err)
|
|
}
|
|
return box
|
|
}
|
|
|
|
func mustUUID(t *testing.T, s string) pgtype.UUID {
|
|
t.Helper()
|
|
u, err := util.ParseUUID(s)
|
|
if err != nil {
|
|
t.Fatalf("parse uuid %q: %v", s, err)
|
|
}
|
|
return u
|
|
}
|
|
|
|
type fakeInstallQueries struct {
|
|
// existing, when set, is the agent's current row; UpsertChannelInstallation
|
|
// returns it (an UPDATE) so a reconnect reuses the same row id.
|
|
existing *db.ChannelInstallation
|
|
// existingAppID is the current row's platform identity returned under lock.
|
|
// It distinguishes a same-robot reconnect from a different-robot replacement.
|
|
existingAppID string
|
|
lockCalled bool
|
|
replaceCalled bool
|
|
replaceParams db.DeleteDingTalkInstallationForReplacementParams
|
|
// appIDTaken makes UpsertChannelInstallation report a unique-constraint
|
|
// violation on the (channel_type, app_id) routing index — i.e. the pasted
|
|
// robot is already connected to a LIVE owner (the reclaim has run by then).
|
|
appIDTaken bool
|
|
upsertParams db.UpsertChannelInstallationParams
|
|
upsertCalled bool
|
|
rowID pgtype.UUID
|
|
|
|
// reclaimedID, when set, is returned by ReclaimDeadChannelInstallationByAppID
|
|
// to model a dead prior owner having been cleared; otherwise it reports
|
|
// pgx.ErrNoRows (nothing was dead). reclaimCalled records that the install
|
|
// path ran the reclaim before upserting.
|
|
reclaimedID *pgtype.UUID
|
|
reclaimCalled bool
|
|
// ownerWorkspaceID / ownerArchived / ownerMissing drive the live-owner lookup
|
|
// that classifies an appIDTaken conflict into the right sentinel.
|
|
ownerWorkspaceID pgtype.UUID
|
|
ownerArchived bool
|
|
ownerMissing bool
|
|
}
|
|
|
|
// WithTx returns the same fake — the fake tx is a no-op token.
|
|
func (f *fakeInstallQueries) WithTx(_ pgx.Tx) installQueries { return f }
|
|
|
|
func (f *fakeInstallQueries) LockDingTalkInstallationOwner(_ context.Context, _ db.LockDingTalkInstallationOwnerParams) error {
|
|
f.lockCalled = true
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeInstallQueries) GetDingTalkInstallationOwnerForUpdate(_ context.Context, _ db.GetDingTalkInstallationOwnerForUpdateParams) (db.GetDingTalkInstallationOwnerForUpdateRow, error) {
|
|
if f.existing == nil {
|
|
return db.GetDingTalkInstallationOwnerForUpdateRow{}, pgx.ErrNoRows
|
|
}
|
|
return db.GetDingTalkInstallationOwnerForUpdateRow{ID: f.existing.ID, AppID: f.existingAppID}, nil
|
|
}
|
|
|
|
func (f *fakeInstallQueries) DeleteDingTalkInstallationForReplacement(_ context.Context, arg db.DeleteDingTalkInstallationForReplacementParams) (pgtype.UUID, error) {
|
|
f.replaceCalled = true
|
|
f.replaceParams = arg
|
|
id := f.existing.ID
|
|
f.existing = nil
|
|
return id, nil
|
|
}
|
|
|
|
func (f *fakeInstallQueries) ReclaimDeadChannelInstallationByAppID(_ context.Context, _ db.ReclaimDeadChannelInstallationByAppIDParams) (pgtype.UUID, error) {
|
|
f.reclaimCalled = true
|
|
if f.reclaimedID != nil {
|
|
return *f.reclaimedID, nil
|
|
}
|
|
return pgtype.UUID{}, pgx.ErrNoRows
|
|
}
|
|
|
|
func (f *fakeInstallQueries) GetChannelInstallationOwnerByAppID(_ context.Context, _ db.GetChannelInstallationOwnerByAppIDParams) (db.GetChannelInstallationOwnerByAppIDRow, error) {
|
|
if f.ownerMissing {
|
|
return db.GetChannelInstallationOwnerByAppIDRow{}, pgx.ErrNoRows
|
|
}
|
|
return db.GetChannelInstallationOwnerByAppIDRow{
|
|
WorkspaceID: f.ownerWorkspaceID,
|
|
AgentArchivedAt: pgtype.Timestamptz{Valid: f.ownerArchived},
|
|
}, nil
|
|
}
|
|
|
|
func (f *fakeInstallQueries) UpsertChannelInstallation(_ context.Context, arg db.UpsertChannelInstallationParams) (db.ChannelInstallation, error) {
|
|
f.upsertCalled = true
|
|
f.upsertParams = arg
|
|
if f.appIDTaken {
|
|
return db.ChannelInstallation{}, &pgconn.PgError{Code: "23505"}
|
|
}
|
|
id := f.rowID
|
|
if f.existing != nil {
|
|
id = f.existing.ID // reconnect updates the agent's existing row in place
|
|
}
|
|
return db.ChannelInstallation{
|
|
ID: id,
|
|
WorkspaceID: arg.WorkspaceID,
|
|
AgentID: arg.AgentID,
|
|
ChannelType: arg.ChannelType,
|
|
Config: arg.Config,
|
|
InstallerUserID: arg.InstallerUserID,
|
|
Status: "active",
|
|
}, nil
|
|
}
|
|
|
|
func (f *fakeInstallQueries) ListChannelInstallationsByWorkspace(_ context.Context, _ db.ListChannelInstallationsByWorkspaceParams) ([]db.ChannelInstallation, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (f *fakeInstallQueries) GetChannelInstallationInWorkspace(_ context.Context, _ db.GetChannelInstallationInWorkspaceParams) (db.ChannelInstallation, error) {
|
|
return db.ChannelInstallation{}, nil
|
|
}
|
|
|
|
func (f *fakeInstallQueries) SetChannelInstallationStatus(_ context.Context, _ db.SetChannelInstallationStatusParams) error {
|
|
return nil
|
|
}
|
|
|
|
// fakeTx is a no-op pgx.Tx: embedding the interface satisfies it, and the
|
|
// install paths only ever call Commit / Rollback.
|
|
type fakeTx struct {
|
|
pgx.Tx
|
|
committed bool
|
|
}
|
|
|
|
func (t *fakeTx) Commit(context.Context) error { t.committed = true; return nil }
|
|
func (t *fakeTx) Rollback(context.Context) error { return nil }
|
|
|
|
type fakeTxStarter struct{ tx *fakeTx }
|
|
|
|
func (f *fakeTxStarter) Begin(context.Context) (pgx.Tx, error) { return f.tx, nil }
|
|
|
|
func newTestInstallService(t *testing.T, q installQueries) *InstallService {
|
|
t.Helper()
|
|
svc, err := newInstallService(q, &fakeTxStarter{tx: &fakeTx{}}, testBox(t), nil)
|
|
if err != nil {
|
|
t.Fatalf("newInstallService: %v", err)
|
|
}
|
|
return svc
|
|
}
|