Files
multica/server/pkg/db/generated/dingtalk.sql.go
T
YYClaw c3577cb04b feat(dingtalk): add DingTalk bot integration (MUL-3958) (#4829)
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/.
2026-08-06 15:36:02 +08:00

136 lines
5.0 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: dingtalk.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const deleteDingTalkInstallationForReplacement = `-- name: DeleteDingTalkInstallationForReplacement :one
WITH retired AS (
DELETE FROM channel_installation ci
WHERE ci.id = $1
AND ci.workspace_id = $2
AND ci.agent_id = $3
AND ci.channel_type = 'dingtalk'
RETURNING ci.id
),
cleared_chat_sessions AS (
DELETE FROM channel_chat_session_binding
WHERE installation_id IN (SELECT id FROM retired)
RETURNING chat_session_id
),
cleared_outbound_cards AS (
DELETE FROM channel_outbound_card_message
WHERE chat_session_id IN (SELECT chat_session_id FROM cleared_chat_sessions)
),
cleared_binding_tokens AS (
DELETE FROM channel_binding_token
WHERE installation_id IN (SELECT id FROM retired)
),
cleared_user_bindings AS (
DELETE FROM channel_user_binding
WHERE installation_id IN (SELECT id FROM retired)
),
cleared_inbound_dedup AS (
DELETE FROM channel_inbound_message_dedup
WHERE installation_id IN (SELECT id FROM retired)
),
detached_audit AS (
UPDATE channel_inbound_audit SET installation_id = NULL
WHERE installation_id IN (SELECT id FROM retired)
),
detached_media_intents AS (
UPDATE channel_media_pending_object SET installation_id = NULL
WHERE installation_id IN (SELECT id FROM retired)
)
SELECT retired.id FROM retired
`
type DeleteDingTalkInstallationForReplacementParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
AgentID pgtype.UUID `json:"agent_id"`
}
// Retires an installation when the same agent is connected with a DIFFERENT
// AppKey. A senderStaffId is scoped to one DingTalk organization, so none of the
// old installation's identity, token, session, dedup, or outbound state may
// cross into the new robot. The caller inserts a fresh installation in the same
// transaction, giving the replacement a new installation_id that also fences
// late writes and replies from the old connection.
//
// Chat sessions themselves remain as history, but their channel bindings and
// outbound cards are removed. Audit and media-intent rows remain useful for
// diagnostics / reconciliation, so their installation references are detached.
func (q *Queries) DeleteDingTalkInstallationForReplacement(ctx context.Context, arg DeleteDingTalkInstallationForReplacementParams) (pgtype.UUID, error) {
row := q.db.QueryRow(ctx, deleteDingTalkInstallationForReplacement, arg.InstallationID, arg.WorkspaceID, arg.AgentID)
var id pgtype.UUID
err := row.Scan(&id)
return id, err
}
const getDingTalkInstallationOwnerForUpdate = `-- name: GetDingTalkInstallationOwnerForUpdate :one
SELECT id, COALESCE(config ->> 'app_id', '')::text AS app_id
FROM channel_installation
WHERE workspace_id = $1
AND agent_id = $2
AND channel_type = 'dingtalk'
FOR UPDATE
`
type GetDingTalkInstallationOwnerForUpdateParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
AgentID pgtype.UUID `json:"agent_id"`
}
type GetDingTalkInstallationOwnerForUpdateRow struct {
ID pgtype.UUID `json:"id"`
AppID string `json:"app_id"`
}
// Reads the current robot identity after LockDingTalkInstallationOwner has
// serialized the logical owner slot. app_id is non-null for every DingTalk
// installation; COALESCE treats malformed legacy config as a different robot,
// which safely replaces it instead of preserving unknown identity state.
func (q *Queries) GetDingTalkInstallationOwnerForUpdate(ctx context.Context, arg GetDingTalkInstallationOwnerForUpdateParams) (GetDingTalkInstallationOwnerForUpdateRow, error) {
row := q.db.QueryRow(ctx, getDingTalkInstallationOwnerForUpdate, arg.WorkspaceID, arg.AgentID)
var i GetDingTalkInstallationOwnerForUpdateRow
err := row.Scan(&i.ID, &i.AppID)
return i, err
}
const lockDingTalkInstallationOwner = `-- name: LockDingTalkInstallationOwner :exec
SELECT pg_advisory_xact_lock(
hashtextextended(
($1::uuid)::text || ':' ||
($2::uuid)::text || ':dingtalk',
0
)
)
`
type LockDingTalkInstallationOwnerParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
AgentID pgtype.UUID `json:"agent_id"`
}
// DingTalk-specific installation identity operations. The underlying channel_*
// tables are shared, but these replacement semantics belong to DingTalk's BYO
// AppKey model and deliberately stay out of the shared channel query surface.
// Serializes install / replacement decisions for one logical
// (workspace, agent, channel) slot. A different-AppKey replacement deletes the
// old row and inserts a fresh installation id; the advisory lock closes the
// gap where two concurrent replacements could otherwise miss each other's new
// row and update it in place, carrying identity state across robot boundaries.
func (q *Queries) LockDingTalkInstallationOwner(ctx context.Context, arg LockDingTalkInstallationOwnerParams) error {
_, err := q.db.Exec(ctx, lockDingTalkInstallationOwner, arg.WorkspaceID, arg.AgentID)
return err
}