feat(access): add guest chat ACL (#235)

This commit is contained in:
BBQ
2026-03-14 17:15:41 +08:00
committed by GitHub
parent c8728ffc2c
commit 839e63acda
86 changed files with 6886 additions and 2554 deletions
+1
View File
@@ -12,6 +12,7 @@ DROP TABLE IF EXISTS bot_history_messages;
DROP TABLE IF EXISTS bot_channel_routes;
DROP TABLE IF EXISTS channel_identity_bind_codes;
DROP TABLE IF EXISTS bot_preauth_keys;
DROP TABLE IF EXISTS bot_acl_rules;
DROP TABLE IF EXISTS bot_channel_configs;
DROP TABLE IF EXISTS mcp_connections;
DROP TABLE IF EXISTS bot_members;
+44 -21
View File
@@ -163,7 +163,6 @@ CREATE TABLE IF NOT EXISTS bots (
max_context_load_time INTEGER NOT NULL DEFAULT 1440,
max_context_tokens INTEGER NOT NULL DEFAULT 0,
language TEXT NOT NULL DEFAULT 'auto',
allow_guest BOOLEAN NOT NULL DEFAULT false,
reasoning_enabled BOOLEAN NOT NULL DEFAULT false,
reasoning_effort TEXT NOT NULL DEFAULT 'medium',
max_inbox_items INTEGER NOT NULL DEFAULT 50,
@@ -186,16 +185,52 @@ CREATE TABLE IF NOT EXISTS bots (
CREATE INDEX IF NOT EXISTS idx_bots_owner_user_id ON bots(owner_user_id);
CREATE TABLE IF NOT EXISTS bot_members (
CREATE TABLE IF NOT EXISTS bot_acl_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member',
action TEXT NOT NULL,
effect TEXT NOT NULL,
subject_kind TEXT NOT NULL,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
channel_identity_id UUID REFERENCES channel_identities(id) ON DELETE CASCADE,
source_channel TEXT,
source_conversation_type TEXT,
source_conversation_id TEXT,
source_thread_id TEXT,
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT bot_members_role_check CHECK (role IN ('owner', 'admin', 'member')),
CONSTRAINT bot_members_unique UNIQUE (bot_id, user_id)
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT bot_acl_rules_action_check CHECK (action IN ('chat.trigger')),
CONSTRAINT bot_acl_rules_effect_check CHECK (effect IN ('allow', 'deny')),
CONSTRAINT bot_acl_rules_subject_kind_check CHECK (subject_kind IN ('guest_all', 'user', 'channel_identity')),
CONSTRAINT bot_acl_rules_source_conversation_type_check CHECK (
source_conversation_type IS NULL OR source_conversation_type IN ('private', 'group', 'thread')
),
CONSTRAINT bot_acl_rules_source_scope_check CHECK (
(source_conversation_id IS NULL AND source_thread_id IS NULL)
OR source_channel IS NOT NULL
),
CONSTRAINT bot_acl_rules_source_thread_check CHECK (
source_thread_id IS NULL OR source_conversation_id IS NOT NULL
),
CONSTRAINT bot_acl_rules_subject_value_check CHECK (
(subject_kind = 'guest_all' AND user_id IS NULL AND channel_identity_id IS NULL) OR
(subject_kind = 'user' AND user_id IS NOT NULL AND channel_identity_id IS NULL) OR
(subject_kind = 'channel_identity' AND user_id IS NULL AND channel_identity_id IS NOT NULL)
),
CONSTRAINT bot_acl_rules_unique_user UNIQUE NULLS NOT DISTINCT (
bot_id, action, effect, subject_kind, user_id,
source_channel, source_conversation_type, source_conversation_id, source_thread_id
),
CONSTRAINT bot_acl_rules_unique_channel_identity UNIQUE NULLS NOT DISTINCT (
bot_id, action, effect, subject_kind, channel_identity_id,
source_channel, source_conversation_type, source_conversation_id, source_thread_id
)
);
CREATE INDEX IF NOT EXISTS idx_bot_members_user_id ON bot_members(user_id);
CREATE INDEX IF NOT EXISTS idx_bot_acl_rules_bot_id ON bot_acl_rules(bot_id);
CREATE INDEX IF NOT EXISTS idx_bot_acl_rules_user_id ON bot_acl_rules(user_id);
CREATE INDEX IF NOT EXISTS idx_bot_acl_rules_channel_identity_id ON bot_acl_rules(channel_identity_id);
CREATE TABLE IF NOT EXISTS mcp_connections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -266,20 +301,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_bot_channel_external_identity
CREATE INDEX IF NOT EXISTS idx_bot_channel_bot_id ON bot_channel_configs(bot_id);
CREATE TABLE IF NOT EXISTS bot_preauth_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
token TEXT NOT NULL,
issued_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
expires_at TIMESTAMPTZ,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT bot_preauth_keys_unique UNIQUE (token)
);
CREATE INDEX IF NOT EXISTS idx_bot_preauth_keys_bot_id ON bot_preauth_keys(bot_id);
CREATE INDEX IF NOT EXISTS idx_bot_preauth_keys_expires ON bot_preauth_keys(expires_at);
-- channel_identity_bind_codes: one-time codes for channel identity->user linking
CREATE TABLE IF NOT EXISTS channel_identity_bind_codes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -338,6 +359,8 @@ CREATE INDEX IF NOT EXISTS idx_bot_history_messages_source_lookup
ON bot_history_messages(channel_type, source_message_id);
CREATE INDEX IF NOT EXISTS idx_bot_history_messages_reply_lookup
ON bot_history_messages(channel_type, source_reply_to_message_id);
CREATE INDEX IF NOT EXISTS idx_bot_history_messages_identity_route_created
ON bot_history_messages(bot_id, sender_channel_identity_id, route_id, created_at DESC);
CREATE TABLE IF NOT EXISTS containers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -0,0 +1,43 @@
-- 0031_chat_acl_remove_bot_members
-- Restore allow_guest, preauth, and bot_members, then drop bot ACL rules.
ALTER TABLE bots ADD COLUMN IF NOT EXISTS allow_guest BOOLEAN NOT NULL DEFAULT false;
UPDATE bots
SET allow_guest = true
WHERE type = 'public'
AND EXISTS (
SELECT 1
FROM bot_acl_rules r
WHERE r.bot_id = bots.id
AND r.action = 'chat.trigger'
AND r.effect = 'allow'
AND r.subject_kind = 'guest_all'
);
CREATE TABLE IF NOT EXISTS bot_preauth_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
token TEXT NOT NULL,
issued_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
expires_at TIMESTAMPTZ,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT bot_preauth_keys_unique UNIQUE (token)
);
CREATE INDEX IF NOT EXISTS idx_bot_preauth_keys_bot_id ON bot_preauth_keys(bot_id);
CREATE INDEX IF NOT EXISTS idx_bot_preauth_keys_expires ON bot_preauth_keys(expires_at);
CREATE TABLE IF NOT EXISTS bot_members (
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT bot_members_role_check CHECK (role IN ('owner', 'admin', 'member')),
CONSTRAINT bot_members_unique UNIQUE (bot_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_bot_members_user_id ON bot_members(user_id);
DROP TABLE IF EXISTS bot_acl_rules;
@@ -0,0 +1,53 @@
-- 0031_chat_acl_remove_bot_members
-- Add bot ACL rules, migrate allow_guest into ACL, and remove legacy bot sharing tables.
CREATE TABLE IF NOT EXISTS bot_acl_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
action TEXT NOT NULL,
effect TEXT NOT NULL,
subject_kind TEXT NOT NULL,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
channel_identity_id UUID REFERENCES channel_identities(id) ON DELETE CASCADE,
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT bot_acl_rules_action_check CHECK (action IN ('chat.trigger')),
CONSTRAINT bot_acl_rules_effect_check CHECK (effect IN ('allow', 'deny')),
CONSTRAINT bot_acl_rules_subject_kind_check CHECK (subject_kind IN ('guest_all', 'user', 'channel_identity')),
CONSTRAINT bot_acl_rules_subject_value_check CHECK (
(subject_kind = 'guest_all' AND user_id IS NULL AND channel_identity_id IS NULL) OR
(subject_kind = 'user' AND user_id IS NOT NULL AND channel_identity_id IS NULL) OR
(subject_kind = 'channel_identity' AND user_id IS NULL AND channel_identity_id IS NOT NULL)
),
CONSTRAINT bot_acl_rules_unique_user UNIQUE NULLS NOT DISTINCT (bot_id, action, effect, subject_kind, user_id),
CONSTRAINT bot_acl_rules_unique_channel_identity UNIQUE NULLS NOT DISTINCT (bot_id, action, effect, subject_kind, channel_identity_id)
);
CREATE INDEX IF NOT EXISTS idx_bot_acl_rules_bot_id ON bot_acl_rules(bot_id);
CREATE INDEX IF NOT EXISTS idx_bot_acl_rules_user_id ON bot_acl_rules(user_id);
CREATE INDEX IF NOT EXISTS idx_bot_acl_rules_channel_identity_id ON bot_acl_rules(channel_identity_id);
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns c
WHERE c.table_schema = 'public'
AND c.table_name = 'bots'
AND c.column_name = 'allow_guest'
) THEN
EXECUTE $migrate$
INSERT INTO bot_acl_rules (bot_id, action, effect, subject_kind, created_by_user_id)
SELECT b.id, 'chat.trigger', 'allow', 'guest_all', b.owner_user_id
FROM bots b
WHERE b.type = 'public'
AND b.allow_guest = true
ON CONFLICT DO NOTHING
$migrate$;
END IF;
END $$;
ALTER TABLE bots DROP COLUMN IF EXISTS allow_guest;
DROP TABLE IF EXISTS bot_preauth_keys;
DROP TABLE IF EXISTS bot_members;
@@ -0,0 +1,52 @@
-- 0032_source_aware_acl_scope
-- Drop source-aware ACL scope fields after ensuring no scoped rules remain.
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM bot_acl_rules
WHERE source_channel IS NOT NULL
OR source_conversation_type IS NOT NULL
OR source_conversation_id IS NOT NULL
OR source_thread_id IS NOT NULL
) THEN
RAISE EXCEPTION 'cannot rollback 0032_source_aware_acl_scope while scoped ACL rules exist';
END IF;
END $$;
DROP INDEX IF EXISTS idx_bot_history_messages_identity_route_created;
ALTER TABLE bot_acl_rules
DROP CONSTRAINT IF EXISTS bot_acl_rules_unique_user,
DROP CONSTRAINT IF EXISTS bot_acl_rules_unique_channel_identity,
DROP CONSTRAINT IF EXISTS bot_acl_rules_source_conversation_type_check,
DROP CONSTRAINT IF EXISTS bot_acl_rules_source_scope_check,
DROP CONSTRAINT IF EXISTS bot_acl_rules_source_thread_check;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'bot_acl_rules_unique_user'
) THEN
ALTER TABLE bot_acl_rules
ADD CONSTRAINT bot_acl_rules_unique_user UNIQUE NULLS NOT DISTINCT (
bot_id, action, effect, subject_kind, user_id
);
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'bot_acl_rules_unique_channel_identity'
) THEN
ALTER TABLE bot_acl_rules
ADD CONSTRAINT bot_acl_rules_unique_channel_identity UNIQUE NULLS NOT DISTINCT (
bot_id, action, effect, subject_kind, channel_identity_id
);
END IF;
END $$;
ALTER TABLE bot_acl_rules
DROP COLUMN IF EXISTS source_channel,
DROP COLUMN IF EXISTS source_conversation_type,
DROP COLUMN IF EXISTS source_conversation_id,
DROP COLUMN IF EXISTS source_thread_id;
@@ -0,0 +1,69 @@
-- 0032_source_aware_acl_scope
-- Add source-aware scope fields to bot ACL rules and index observed conversations.
ALTER TABLE bot_acl_rules
ADD COLUMN IF NOT EXISTS source_channel TEXT,
ADD COLUMN IF NOT EXISTS source_conversation_type TEXT,
ADD COLUMN IF NOT EXISTS source_conversation_id TEXT,
ADD COLUMN IF NOT EXISTS source_thread_id TEXT;
ALTER TABLE bot_acl_rules
DROP CONSTRAINT IF EXISTS bot_acl_rules_unique_user,
DROP CONSTRAINT IF EXISTS bot_acl_rules_unique_channel_identity,
DROP CONSTRAINT IF EXISTS bot_acl_rules_source_conversation_type_check,
DROP CONSTRAINT IF EXISTS bot_acl_rules_source_scope_check,
DROP CONSTRAINT IF EXISTS bot_acl_rules_source_thread_check;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'bot_acl_rules_source_conversation_type_check'
) THEN
ALTER TABLE bot_acl_rules
ADD CONSTRAINT bot_acl_rules_source_conversation_type_check CHECK (
source_conversation_type IS NULL OR source_conversation_type IN ('private', 'group', 'thread')
);
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'bot_acl_rules_source_scope_check'
) THEN
ALTER TABLE bot_acl_rules
ADD CONSTRAINT bot_acl_rules_source_scope_check CHECK (
(source_conversation_id IS NULL AND source_thread_id IS NULL)
OR source_channel IS NOT NULL
);
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'bot_acl_rules_source_thread_check'
) THEN
ALTER TABLE bot_acl_rules
ADD CONSTRAINT bot_acl_rules_source_thread_check CHECK (
source_thread_id IS NULL OR source_conversation_id IS NOT NULL
);
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'bot_acl_rules_unique_user'
) THEN
ALTER TABLE bot_acl_rules
ADD CONSTRAINT bot_acl_rules_unique_user UNIQUE NULLS NOT DISTINCT (
bot_id, action, effect, subject_kind, user_id,
source_channel, source_conversation_type, source_conversation_id, source_thread_id
);
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'bot_acl_rules_unique_channel_identity'
) THEN
ALTER TABLE bot_acl_rules
ADD CONSTRAINT bot_acl_rules_unique_channel_identity UNIQUE NULLS NOT DISTINCT (
bot_id, action, effect, subject_kind, channel_identity_id,
source_channel, source_conversation_type, source_conversation_id, source_thread_id
);
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_bot_history_messages_identity_route_created
ON bot_history_messages(bot_id, sender_channel_identity_id, route_id, created_at DESC);
+136
View File
@@ -0,0 +1,136 @@
-- name: UpsertBotACLGuestAllAllowRule :one
INSERT INTO bot_acl_rules (bot_id, action, effect, subject_kind, created_by_user_id)
VALUES ($1, 'chat.trigger', 'allow', 'guest_all', $2)
ON CONFLICT ON CONSTRAINT bot_acl_rules_unique_user
DO UPDATE SET
created_by_user_id = COALESCE(EXCLUDED.created_by_user_id, bot_acl_rules.created_by_user_id),
updated_at = now()
RETURNING id, bot_id, action, effect, subject_kind, user_id, channel_identity_id, source_channel, source_conversation_type, source_conversation_id, source_thread_id, created_by_user_id, created_at, updated_at;
-- name: UpsertBotACLUserRule :one
INSERT INTO bot_acl_rules (
bot_id, action, effect, subject_kind, user_id,
source_channel, source_conversation_type, source_conversation_id, source_thread_id,
created_by_user_id
)
VALUES (
$1, 'chat.trigger', $2, 'user', $3,
sqlc.narg(source_channel)::text,
sqlc.narg(source_conversation_type)::text,
sqlc.narg(source_conversation_id)::text,
sqlc.narg(source_thread_id)::text,
$4
)
ON CONFLICT ON CONSTRAINT bot_acl_rules_unique_user
DO UPDATE SET
created_by_user_id = COALESCE(EXCLUDED.created_by_user_id, bot_acl_rules.created_by_user_id),
updated_at = now()
RETURNING id, bot_id, action, effect, subject_kind, user_id, channel_identity_id, source_channel, source_conversation_type, source_conversation_id, source_thread_id, created_by_user_id, created_at, updated_at;
-- name: UpsertBotACLChannelIdentityRule :one
INSERT INTO bot_acl_rules (
bot_id, action, effect, subject_kind, channel_identity_id,
source_channel, source_conversation_type, source_conversation_id, source_thread_id,
created_by_user_id
)
VALUES (
$1, 'chat.trigger', $2, 'channel_identity', $3,
sqlc.narg(source_channel)::text,
sqlc.narg(source_conversation_type)::text,
sqlc.narg(source_conversation_id)::text,
sqlc.narg(source_thread_id)::text,
$4
)
ON CONFLICT ON CONSTRAINT bot_acl_rules_unique_channel_identity
DO UPDATE SET
created_by_user_id = COALESCE(EXCLUDED.created_by_user_id, bot_acl_rules.created_by_user_id),
updated_at = now()
RETURNING id, bot_id, action, effect, subject_kind, user_id, channel_identity_id, source_channel, source_conversation_type, source_conversation_id, source_thread_id, created_by_user_id, created_at, updated_at;
-- name: DeleteBotACLGuestAllAllowRule :exec
DELETE FROM bot_acl_rules
WHERE bot_id = $1
AND action = 'chat.trigger'
AND effect = 'allow'
AND subject_kind = 'guest_all';
-- name: DeleteBotACLRuleByID :exec
DELETE FROM bot_acl_rules
WHERE id = $1;
-- name: HasBotACLGuestAllAllowRule :one
SELECT EXISTS (
SELECT 1
FROM bot_acl_rules
WHERE bot_id = $1
AND action = 'chat.trigger'
AND effect = 'allow'
AND subject_kind = 'guest_all'
) AS allowed;
-- name: HasBotACLUserRule :one
SELECT EXISTS (
SELECT 1
FROM bot_acl_rules
WHERE bot_id = $1
AND action = 'chat.trigger'
AND effect = $2
AND subject_kind = 'user'
AND user_id = $3
AND (source_channel IS NULL OR source_channel = sqlc.narg(source_channel)::text)
AND (source_conversation_type IS NULL OR source_conversation_type = sqlc.narg(source_conversation_type)::text)
AND (source_conversation_id IS NULL OR source_conversation_id = sqlc.narg(source_conversation_id)::text)
AND (source_thread_id IS NULL OR source_thread_id = sqlc.narg(source_thread_id)::text)
) AS matched;
-- name: HasBotACLChannelIdentityRule :one
SELECT EXISTS (
SELECT 1
FROM bot_acl_rules
WHERE bot_id = $1
AND action = 'chat.trigger'
AND effect = $2
AND subject_kind = 'channel_identity'
AND channel_identity_id = $3
AND (source_channel IS NULL OR source_channel = sqlc.narg(source_channel)::text)
AND (source_conversation_type IS NULL OR source_conversation_type = sqlc.narg(source_conversation_type)::text)
AND (source_conversation_id IS NULL OR source_conversation_id = sqlc.narg(source_conversation_id)::text)
AND (source_thread_id IS NULL OR source_thread_id = sqlc.narg(source_thread_id)::text)
) AS matched;
-- name: ListBotACLSubjectRulesByEffect :many
SELECT
r.id,
r.bot_id,
r.action,
r.effect,
r.subject_kind,
r.user_id,
r.channel_identity_id,
r.source_channel,
r.source_conversation_type,
r.source_conversation_id,
r.source_thread_id,
r.created_by_user_id,
r.created_at,
r.updated_at,
u.username AS user_username,
u.display_name AS user_display_name,
u.avatar_url AS user_avatar_url,
ci.channel_type,
ci.channel_subject_id,
ci.display_name AS channel_identity_display_name,
ci.avatar_url AS channel_identity_avatar_url,
linked.id AS linked_user_id,
linked.username AS linked_user_username,
linked.display_name AS linked_user_display_name,
linked.avatar_url AS linked_user_avatar_url
FROM bot_acl_rules r
LEFT JOIN users u ON u.id = r.user_id
LEFT JOIN channel_identities ci ON ci.id = r.channel_identity_id
LEFT JOIN users linked ON linked.id = ci.user_id
WHERE r.bot_id = $1
AND r.action = 'chat.trigger'
AND r.effect = $2
AND r.subject_kind IN ('user', 'channel_identity')
ORDER BY r.created_at DESC;
+5 -34
View File
@@ -1,26 +1,19 @@
-- name: CreateBot :one
INSERT INTO bots (owner_user_id, type, display_name, avatar_url, is_active, metadata, status)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, max_inbox_items, language, allow_guest, reasoning_enabled, reasoning_effort, chat_model_id, search_provider_id, memory_provider_id, heartbeat_enabled, heartbeat_interval, heartbeat_prompt, metadata, created_at, updated_at;
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, max_inbox_items, language, reasoning_enabled, reasoning_effort, chat_model_id, search_provider_id, memory_provider_id, heartbeat_enabled, heartbeat_interval, heartbeat_prompt, metadata, created_at, updated_at;
-- name: GetBotByID :one
SELECT id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, max_inbox_items, language, allow_guest, reasoning_enabled, reasoning_effort, chat_model_id, search_provider_id, memory_provider_id, heartbeat_enabled, heartbeat_interval, heartbeat_prompt, metadata, created_at, updated_at
SELECT id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, max_inbox_items, language, reasoning_enabled, reasoning_effort, chat_model_id, search_provider_id, memory_provider_id, heartbeat_enabled, heartbeat_interval, heartbeat_prompt, metadata, created_at, updated_at
FROM bots
WHERE id = $1;
-- name: ListBotsByOwner :many
SELECT id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, max_inbox_items, language, allow_guest, reasoning_enabled, reasoning_effort, chat_model_id, search_provider_id, memory_provider_id, heartbeat_enabled, heartbeat_interval, heartbeat_prompt, metadata, created_at, updated_at
SELECT id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, max_inbox_items, language, reasoning_enabled, reasoning_effort, chat_model_id, search_provider_id, memory_provider_id, heartbeat_enabled, heartbeat_interval, heartbeat_prompt, metadata, created_at, updated_at
FROM bots
WHERE owner_user_id = $1
ORDER BY created_at DESC;
-- name: ListBotsByMember :many
SELECT b.id, b.owner_user_id, b.type, b.display_name, b.avatar_url, b.is_active, b.status, b.max_context_load_time, b.max_context_tokens, b.max_inbox_items, b.language, b.allow_guest, b.reasoning_enabled, b.reasoning_effort, b.chat_model_id, b.search_provider_id, b.memory_provider_id, b.heartbeat_enabled, b.heartbeat_interval, b.heartbeat_prompt, b.metadata, b.created_at, b.updated_at
FROM bots b
JOIN bot_members m ON m.bot_id = b.id
WHERE m.user_id = $1
ORDER BY b.created_at DESC;
-- name: UpdateBotProfile :one
UPDATE bots
SET display_name = $2,
@@ -29,14 +22,14 @@ SET display_name = $2,
metadata = $5,
updated_at = now()
WHERE id = $1
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, max_inbox_items, language, allow_guest, reasoning_enabled, reasoning_effort, chat_model_id, search_provider_id, memory_provider_id, heartbeat_enabled, heartbeat_interval, heartbeat_prompt, metadata, created_at, updated_at;
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, max_inbox_items, language, reasoning_enabled, reasoning_effort, chat_model_id, search_provider_id, memory_provider_id, heartbeat_enabled, heartbeat_interval, heartbeat_prompt, metadata, created_at, updated_at;
-- name: UpdateBotOwner :one
UPDATE bots
SET owner_user_id = $2,
updated_at = now()
WHERE id = $1
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, max_inbox_items, language, allow_guest, reasoning_enabled, reasoning_effort, chat_model_id, search_provider_id, memory_provider_id, heartbeat_enabled, heartbeat_interval, heartbeat_prompt, metadata, created_at, updated_at;
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, max_inbox_items, language, reasoning_enabled, reasoning_effort, chat_model_id, search_provider_id, memory_provider_id, heartbeat_enabled, heartbeat_interval, heartbeat_prompt, metadata, created_at, updated_at;
-- name: UpdateBotStatus :exec
UPDATE bots
@@ -47,28 +40,6 @@ WHERE id = $1;
-- name: DeleteBotByID :exec
DELETE FROM bots WHERE id = $1;
-- name: UpsertBotMember :one
INSERT INTO bot_members (bot_id, user_id, role)
VALUES ($1, $2, $3)
ON CONFLICT (bot_id, user_id) DO UPDATE SET
role = EXCLUDED.role
RETURNING bot_id, user_id, role, created_at;
-- name: ListBotMembers :many
SELECT bot_id, user_id, role, created_at
FROM bot_members
WHERE bot_id = $1
ORDER BY created_at DESC;
-- name: GetBotMember :one
SELECT bot_id, user_id, role, created_at
FROM bot_members
WHERE bot_id = $1 AND user_id = $2
LIMIT 1;
-- name: DeleteBotMember :exec
DELETE FROM bot_members WHERE bot_id = $1 AND user_id = $2;
-- name: ListHeartbeatEnabledBots :many
SELECT id, owner_user_id, heartbeat_enabled, heartbeat_interval, heartbeat_prompt
FROM bots
+26
View File
@@ -37,6 +37,32 @@ FROM channel_identities
WHERE user_id = $1
ORDER BY created_at DESC;
-- name: SearchChannelIdentities :many
SELECT
ci.id,
ci.user_id,
ci.channel_type,
ci.channel_subject_id,
ci.display_name,
ci.avatar_url,
ci.metadata,
ci.created_at,
ci.updated_at,
u.username AS linked_username,
u.display_name AS linked_display_name,
u.avatar_url AS linked_avatar_url
FROM channel_identities ci
LEFT JOIN users u ON u.id = ci.user_id
WHERE
sqlc.arg(query)::text = ''
OR ci.channel_type ILIKE '%' || sqlc.arg(query)::text || '%'
OR ci.channel_subject_id ILIKE '%' || sqlc.arg(query)::text || '%'
OR COALESCE(ci.display_name, '') ILIKE '%' || sqlc.arg(query)::text || '%'
OR COALESCE(u.username, '') ILIKE '%' || sqlc.arg(query)::text || '%'
OR COALESCE(u.display_name, '') ILIKE '%' || sqlc.arg(query)::text || '%'
ORDER BY ci.updated_at DESC
LIMIT sqlc.arg(limit_count);
-- name: SetChannelIdentityLinkedUser :one
UPDATE channel_identities
SET user_id = $2, updated_at = now()
+17 -63
View File
@@ -44,10 +44,9 @@ SELECT
b.created_at,
b.updated_at
FROM bots b
LEFT JOIN bot_members bm ON bm.bot_id = b.id AND bm.user_id = sqlc.arg(user_id)
LEFT JOIN models chat_models ON chat_models.id = b.chat_model_id
WHERE b.id = sqlc.arg(bot_id)
AND (b.owner_user_id = sqlc.arg(user_id) OR bm.user_id IS NOT NULL)
AND b.owner_user_id = sqlc.arg(user_id)
ORDER BY b.updated_at DESC;
-- name: ListVisibleChatsByBotAndUser :many
@@ -69,24 +68,19 @@ SELECT
END)::text AS participant_role,
NULL::timestamptz AS last_observed_at
FROM bots b
LEFT JOIN bot_members bm ON bm.bot_id = b.id AND bm.user_id = sqlc.arg(user_id)
LEFT JOIN models chat_models ON chat_models.id = b.chat_model_id
WHERE b.id = sqlc.arg(bot_id)
AND (b.owner_user_id = sqlc.arg(user_id) OR bm.user_id IS NOT NULL)
AND b.owner_user_id = sqlc.arg(user_id)
ORDER BY b.updated_at DESC;
-- name: GetChatReadAccessByUser :one
SELECT
'participant'::text AS access_mode,
(CASE
WHEN b.owner_user_id = sqlc.arg(user_id) THEN 'owner'
ELSE COALESCE(bm.role, ''::text)
END)::text AS participant_role,
'owner'::text AS participant_role,
NULL::timestamptz AS last_observed_at
FROM bots b
LEFT JOIN bot_members bm ON bm.bot_id = b.id AND bm.user_id = sqlc.arg(user_id)
WHERE b.id = sqlc.arg(chat_id)
AND (b.owner_user_id = sqlc.arg(user_id) OR bm.user_id IS NOT NULL)
AND b.owner_user_id = sqlc.arg(user_id)
LIMIT 1;
-- name: ListThreadsByParent :many
@@ -141,66 +135,26 @@ WITH deleted_messages AS (
DELETE FROM bot_channel_routes bcr
WHERE bcr.bot_id = sqlc.arg(chat_id);
-- chat_participants
-- name: AddChatParticipant :one
INSERT INTO bot_members (bot_id, user_id, role)
VALUES (sqlc.arg(chat_id), sqlc.arg(user_id), sqlc.arg(role))
ON CONFLICT (bot_id, user_id) DO UPDATE SET role = EXCLUDED.role
RETURNING bot_id AS chat_id, user_id, role, created_at AS joined_at;
-- name: GetChatParticipant :one
WITH owner_participant AS (
SELECT b.id AS chat_id, b.owner_user_id AS user_id, 'owner'::text AS role, b.created_at AS joined_at
FROM bots b
WHERE b.id = sqlc.arg(chat_id) AND b.owner_user_id = sqlc.arg(user_id)
),
member_participant AS (
SELECT bm.bot_id AS chat_id, bm.user_id, bm.role, bm.created_at AS joined_at
FROM bot_members bm
WHERE bm.bot_id = sqlc.arg(chat_id) AND bm.user_id = sqlc.arg(user_id)
)
SELECT chat_id, user_id, role, joined_at
FROM (
SELECT * FROM owner_participant
UNION ALL
SELECT * FROM member_participant
) p
ORDER BY CASE WHEN role = 'owner' THEN 0 ELSE 1 END
SELECT b.id AS chat_id, b.owner_user_id AS user_id, 'owner'::text AS role, b.created_at AS joined_at
FROM bots b
WHERE b.id = sqlc.arg(chat_id) AND b.owner_user_id = sqlc.arg(user_id)
LIMIT 1;
-- name: ListChatParticipants :many
WITH owner_participant AS (
SELECT b.id AS chat_id, b.owner_user_id AS user_id, 'owner'::text AS role, b.created_at AS joined_at
FROM bots b
WHERE b.id = sqlc.arg(chat_id)
),
member_participant AS (
SELECT bm.bot_id AS chat_id, bm.user_id, bm.role, bm.created_at AS joined_at
FROM bot_members bm
WHERE bm.bot_id = sqlc.arg(chat_id)
AND bm.user_id <> (SELECT owner_user_id FROM bots WHERE id = sqlc.arg(chat_id))
)
SELECT chat_id, user_id, role, joined_at
FROM (
SELECT * FROM owner_participant
UNION ALL
SELECT * FROM member_participant
) p
SELECT b.id AS chat_id, b.owner_user_id AS user_id, 'owner'::text AS role, b.created_at AS joined_at
FROM bots b
WHERE b.id = sqlc.arg(chat_id)
ORDER BY joined_at ASC;
-- name: RemoveChatParticipant :exec
DELETE FROM bot_members
WHERE bot_id = sqlc.arg(chat_id)
AND user_id = sqlc.arg(user_id)
AND user_id <> (SELECT owner_user_id FROM bots WHERE id = sqlc.arg(chat_id));
-- name: CopyParticipantsToChat :exec
INSERT INTO bot_members (bot_id, user_id, role)
SELECT sqlc.arg(chat_id_2), bm.user_id, bm.role
FROM bot_members bm
WHERE bm.bot_id = sqlc.arg(chat_id)
ON CONFLICT (bot_id, user_id) DO NOTHING;
SELECT 1
WHERE EXISTS (
SELECT 1
FROM bots b
WHERE b.id = sqlc.arg(chat_id)
AND b.owner_user_id = sqlc.arg(user_id)
);
-- chat_settings
+22
View File
@@ -162,3 +162,25 @@ LIMIT sqlc.arg(max_count);
-- name: DeleteMessagesByBot :exec
DELETE FROM bot_history_messages
WHERE bot_id = sqlc.arg(bot_id);
-- name: ListObservedConversationsByChannelIdentity :many
SELECT
r.id AS route_id,
r.channel_type AS channel,
COALESCE(r.conversation_type, '') AS conversation_type,
r.external_conversation_id AS conversation_id,
COALESCE(r.external_thread_id, '') AS thread_id,
COALESCE(r.metadata->>'conversation_name', '')::text AS conversation_name,
MAX(m.created_at)::timestamptz AS last_observed_at
FROM bot_history_messages m
JOIN bot_channel_routes r ON r.id = m.route_id
WHERE m.bot_id = sqlc.arg(bot_id)
AND m.sender_channel_identity_id = sqlc.arg(channel_identity_id)
GROUP BY
r.id,
r.channel_type,
r.conversation_type,
r.external_conversation_id,
r.external_thread_id,
r.metadata
ORDER BY MAX(m.created_at) DESC;
-18
View File
@@ -1,18 +0,0 @@
-- name: CreateBotPreauthKey :one
INSERT INTO bot_preauth_keys (bot_id, token, issued_by_user_id, expires_at)
VALUES ($1, $2, $3, $4)
RETURNING id, bot_id, token, issued_by_user_id, expires_at, used_at, created_at;
-- name: GetBotPreauthKey :one
SELECT id, bot_id, token, issued_by_user_id, expires_at, used_at, created_at
FROM bot_preauth_keys
WHERE token = $1
AND used_at IS NULL
AND (expires_at IS NULL OR expires_at > now())
LIMIT 1;
-- name: MarkBotPreauthKeyUsed :one
UPDATE bot_preauth_keys
SET used_at = now()
WHERE id = $1
RETURNING id, bot_id, token, issued_by_user_id, expires_at, used_at, created_at;
+1 -5
View File
@@ -5,7 +5,6 @@ SELECT
bots.max_context_tokens,
bots.max_inbox_items,
bots.language,
bots.allow_guest,
bots.reasoning_enabled,
bots.reasoning_effort,
bots.heartbeat_enabled,
@@ -33,7 +32,6 @@ WITH updated AS (
max_context_tokens = sqlc.arg(max_context_tokens),
max_inbox_items = sqlc.arg(max_inbox_items),
language = sqlc.arg(language),
allow_guest = sqlc.arg(allow_guest),
reasoning_enabled = sqlc.arg(reasoning_enabled),
reasoning_effort = sqlc.arg(reasoning_effort),
heartbeat_enabled = sqlc.arg(heartbeat_enabled),
@@ -47,7 +45,7 @@ WITH updated AS (
browser_context_id = COALESCE(sqlc.narg(browser_context_id)::uuid, bots.browser_context_id),
updated_at = now()
WHERE bots.id = sqlc.arg(id)
RETURNING bots.id, bots.max_context_load_time, bots.max_context_tokens, bots.max_inbox_items, bots.language, bots.allow_guest, bots.reasoning_enabled, bots.reasoning_effort, bots.heartbeat_enabled, bots.heartbeat_interval, bots.heartbeat_prompt, bots.chat_model_id, bots.heartbeat_model_id, bots.search_provider_id, bots.memory_provider_id, bots.tts_model_id, bots.browser_context_id
RETURNING bots.id, bots.max_context_load_time, bots.max_context_tokens, bots.max_inbox_items, bots.language, bots.reasoning_enabled, bots.reasoning_effort, bots.heartbeat_enabled, bots.heartbeat_interval, bots.heartbeat_prompt, bots.chat_model_id, bots.heartbeat_model_id, bots.search_provider_id, bots.memory_provider_id, bots.tts_model_id, bots.browser_context_id
)
SELECT
updated.id AS bot_id,
@@ -55,7 +53,6 @@ SELECT
updated.max_context_tokens,
updated.max_inbox_items,
updated.language,
updated.allow_guest,
updated.reasoning_enabled,
updated.reasoning_effort,
updated.heartbeat_enabled,
@@ -81,7 +78,6 @@ SET max_context_load_time = 1440,
max_context_tokens = 0,
max_inbox_items = 50,
language = 'auto',
allow_guest = false,
reasoning_enabled = false,
reasoning_effort = 'medium',
heartbeat_enabled = false,
+13
View File
@@ -64,6 +64,19 @@ SELECT * FROM users
WHERE username IS NOT NULL
ORDER BY created_at DESC;
-- name: SearchAccounts :many
SELECT *
FROM users
WHERE username IS NOT NULL
AND (
sqlc.arg(query)::text = ''
OR username ILIKE '%' || sqlc.arg(query)::text || '%'
OR COALESCE(display_name, '') ILIKE '%' || sqlc.arg(query)::text || '%'
OR COALESCE(email, '') ILIKE '%' || sqlc.arg(query)::text || '%'
)
ORDER BY last_login_at DESC NULLS LAST, created_at DESC
LIMIT sqlc.arg(limit_count);
-- name: UpdateAccountProfile :one
UPDATE users
SET display_name = $2,