diff --git a/backend/open_webui/models/channels.py b/backend/open_webui/models/channels.py index dc13d362d8..e95b6f6c95 100644 --- a/backend/open_webui/models/channels.py +++ b/backend/open_webui/models/channels.py @@ -1,4 +1,5 @@ import json +import secrets import time import uuid from typing import Optional @@ -245,6 +246,11 @@ class CreateChannelForm(ChannelForm): type: Optional[str] = None +class ChannelWebhookForm(BaseModel): + name: str + profile_image_url: Optional[str] = None + + class ChannelTable: def _collect_unique_user_ids( @@ -945,5 +951,115 @@ class ChannelTable: db.commit() return True + #################### + # Webhook Methods + #################### + + def insert_webhook( + self, + channel_id: str, + user_id: str, + form_data: ChannelWebhookForm, + db: Optional[Session] = None, + ) -> Optional[ChannelWebhookModel]: + with get_db_context(db) as db: + webhook = ChannelWebhookModel( + id=str(uuid.uuid4()), + channel_id=channel_id, + user_id=user_id, + name=form_data.name, + profile_image_url=form_data.profile_image_url, + token=secrets.token_urlsafe(32), + last_used_at=None, + created_at=int(time.time_ns()), + updated_at=int(time.time_ns()), + ) + db.add(ChannelWebhook(**webhook.model_dump())) + db.commit() + return webhook + + def get_webhooks_by_channel_id( + self, channel_id: str, db: Optional[Session] = None + ) -> list[ChannelWebhookModel]: + with get_db_context(db) as db: + webhooks = ( + db.query(ChannelWebhook) + .filter(ChannelWebhook.channel_id == channel_id) + .all() + ) + return [ChannelWebhookModel.model_validate(w) for w in webhooks] + + def get_webhook_by_id( + self, webhook_id: str, db: Optional[Session] = None + ) -> Optional[ChannelWebhookModel]: + with get_db_context(db) as db: + webhook = ( + db.query(ChannelWebhook) + .filter(ChannelWebhook.id == webhook_id) + .first() + ) + return ChannelWebhookModel.model_validate(webhook) if webhook else None + + def get_webhook_by_id_and_token( + self, webhook_id: str, token: str, db: Optional[Session] = None + ) -> Optional[ChannelWebhookModel]: + with get_db_context(db) as db: + webhook = ( + db.query(ChannelWebhook) + .filter( + ChannelWebhook.id == webhook_id, + ChannelWebhook.token == token, + ) + .first() + ) + return ChannelWebhookModel.model_validate(webhook) if webhook else None + + def update_webhook_by_id( + self, + webhook_id: str, + form_data: ChannelWebhookForm, + db: Optional[Session] = None, + ) -> Optional[ChannelWebhookModel]: + with get_db_context(db) as db: + webhook = ( + db.query(ChannelWebhook) + .filter(ChannelWebhook.id == webhook_id) + .first() + ) + if not webhook: + return None + webhook.name = form_data.name + webhook.profile_image_url = form_data.profile_image_url + webhook.updated_at = int(time.time_ns()) + db.commit() + return ChannelWebhookModel.model_validate(webhook) + + def update_webhook_last_used_at( + self, webhook_id: str, db: Optional[Session] = None + ) -> bool: + with get_db_context(db) as db: + webhook = ( + db.query(ChannelWebhook) + .filter(ChannelWebhook.id == webhook_id) + .first() + ) + if not webhook: + return False + webhook.last_used_at = int(time.time_ns()) + db.commit() + return True + + def delete_webhook_by_id( + self, webhook_id: str, db: Optional[Session] = None + ) -> bool: + with get_db_context(db) as db: + result = ( + db.query(ChannelWebhook) + .filter(ChannelWebhook.id == webhook_id) + .delete() + ) + db.commit() + return result > 0 + Channels = ChannelTable() diff --git a/backend/open_webui/models/messages.py b/backend/open_webui/models/messages.py index a4c91f3acb..0851107b0b 100644 --- a/backend/open_webui/models/messages.py +++ b/backend/open_webui/models/messages.py @@ -199,11 +199,32 @@ class MessageTable: if include_thread_replies: thread_replies = self.get_thread_replies_by_message_id(id, db=db) - user = Users.get_user_by_id(message.user_id, db=db) + # Check if message was sent by webhook (webhook info in meta takes precedence) + webhook_info = message.meta.get("webhook") if message.meta else None + if webhook_info and webhook_info.get("id"): + # Look up webhook by ID to get current name + webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db) + if webhook: + user_info = { + "id": webhook.id, + "name": webhook.name, + "role": "webhook", + } + else: + # Webhook was deleted, use placeholder + user_info = { + "id": webhook_info.get("id"), + "name": "Deleted Webhook", + "role": "webhook", + } + else: + user = Users.get_user_by_id(message.user_id, db=db) + user_info = user.model_dump() if user else None + return MessageResponse.model_validate( { **MessageModel.model_validate(message).model_dump(), - "user": user.model_dump() if user else None, + "user": user_info, "reply_to_message": ( reply_to_message.model_dump() if reply_to_message else None ), @@ -235,10 +256,29 @@ class MessageTable: if message.reply_to_id else None ) + + webhook_info = message.meta.get("webhook") if message.meta else None + user_info = None + if webhook_info and webhook_info.get("id"): + webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db) + if webhook: + user_info = { + "id": webhook.id, + "name": webhook.name, + "role": "webhook", + } + else: + user_info = { + "id": webhook_info.get("id"), + "name": "Deleted Webhook", + "role": "webhook", + } + messages.append( MessageReplyToResponse.model_validate( { **MessageModel.model_validate(message).model_dump(), + "user": user_info, "reply_to_message": ( reply_to_message.model_dump() if reply_to_message @@ -284,10 +324,29 @@ class MessageTable: if message.reply_to_id else None ) + + webhook_info = message.meta.get("webhook") if message.meta else None + user_info = None + if webhook_info and webhook_info.get("id"): + webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db) + if webhook: + user_info = { + "id": webhook.id, + "name": webhook.name, + "role": "webhook", + } + else: + user_info = { + "id": webhook_info.get("id"), + "name": "Deleted Webhook", + "role": "webhook", + } + messages.append( MessageReplyToResponse.model_validate( { **MessageModel.model_validate(message).model_dump(), + "user": user_info, "reply_to_message": ( reply_to_message.model_dump() if reply_to_message @@ -334,10 +393,29 @@ class MessageTable: if message.reply_to_id else None ) + + webhook_info = message.meta.get("webhook") if message.meta else None + user_info = None + if webhook_info and webhook_info.get("id"): + webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db) + if webhook: + user_info = { + "id": webhook.id, + "name": webhook.name, + "role": "webhook", + } + else: + user_info = { + "id": webhook_info.get("id"), + "name": "Deleted Webhook", + "role": "webhook", + } + messages.append( MessageReplyToResponse.model_validate( { **MessageModel.model_validate(message).model_dump(), + "user": user_info, "reply_to_message": ( reply_to_message.model_dump() if reply_to_message diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index a2ff614524..d501818b76 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -1,9 +1,12 @@ import json import logging +import base64 +import io from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks +from fastapi.responses import Response, StreamingResponse, FileResponse from pydantic import BaseModel from pydantic import field_validator @@ -29,6 +32,8 @@ from open_webui.models.channels import ( ChannelForm, ChannelResponse, CreateChannelForm, + ChannelWebhookModel, + ChannelWebhookForm, ) from open_webui.models.messages import ( Messages, @@ -43,6 +48,7 @@ from open_webui.utils.files import get_image_base64_from_file_id from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT from open_webui.constants import ERROR_MESSAGES +from open_webui.env import STATIC_DIR from open_webui.utils.models import ( @@ -822,6 +828,11 @@ async def get_channel_messages( thread_replies[0].created_at if thread_replies else None ) + # Use message.user if present (for webhooks), otherwise look up by user_id + user_info = message.user + if user_info is None and message.user_id in users: + user_info = UserNameResponse(**users[message.user_id].model_dump()) + messages.append( MessageUserResponse( **{ @@ -831,7 +842,7 @@ async def get_channel_messages( "reactions": Messages.get_reactions_by_message_id( message.id, db=db ), - "user": UserNameResponse(**users[message.user_id].model_dump()), + "user": user_info, } ) ) @@ -889,6 +900,19 @@ async def get_pinned_channel_messages( messages = [] for message in message_list: + # Check for webhook identity in meta + webhook_info = message.meta.get("webhook") if message.meta else None + if webhook_info: + user_info = UserNameResponse( + id=webhook_info.get("id"), + name=webhook_info.get("name"), + role="webhook", + ) + elif message.user_id in users: + user_info = UserNameResponse(**users[message.user_id].model_dump()) + else: + user_info = None + messages.append( MessageWithReactionsResponse( **{ @@ -896,7 +920,7 @@ async def get_pinned_channel_messages( "reactions": Messages.get_reactions_by_message_id( message.id, db=db ), - "user": UserNameResponse(**users[message.user_id].model_dump()), + "user": user_info, } ) ) @@ -1476,6 +1500,11 @@ async def get_channel_thread_messages( messages = [] for message in message_list: + # Use message.user if present (for webhooks), otherwise look up by user_id + user_info = message.user + if user_info is None and message.user_id in users: + user_info = UserNameResponse(**users[message.user_id].model_dump()) + messages.append( MessageUserResponse( **{ @@ -1485,7 +1514,7 @@ async def get_channel_thread_messages( "reactions": Messages.get_reactions_by_message_id( message.id, db=db ), - "user": UserNameResponse(**users[message.user_id].model_dump()), + "user": user_info, } ) ) @@ -1835,3 +1864,262 @@ async def delete_message_by_id( raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() ) + + +############################ +# Webhooks +############################ + + +@router.get("/webhooks/{webhook_id}/profile/image") +async def get_webhook_profile_image( + webhook_id: str, + user=Depends(get_verified_user), + db: Session = Depends(get_session), +): + """Get webhook profile image by webhook ID.""" + webhook = Channels.get_webhook_by_id(webhook_id, db=db) + if not webhook: + # Return default favicon if webhook not found + return FileResponse(f"{STATIC_DIR}/favicon.png") + + if webhook.profile_image_url: + # Check if it's url or base64 + if webhook.profile_image_url.startswith("http"): + return Response( + status_code=status.HTTP_302_FOUND, + headers={"Location": webhook.profile_image_url}, + ) + elif webhook.profile_image_url.startswith("data:image"): + try: + header, base64_data = webhook.profile_image_url.split(",", 1) + image_data = base64.b64decode(base64_data) + image_buffer = io.BytesIO(image_data) + media_type = header.split(";")[0].lstrip("data:") + + return StreamingResponse( + image_buffer, + media_type=media_type, + headers={"Content-Disposition": "inline"}, + ) + except Exception as e: + pass + + # Return default favicon if no profile image + return FileResponse(f"{STATIC_DIR}/favicon.png") + +@router.get("/{id}/webhooks", response_model=list[ChannelWebhookModel]) +async def get_channel_webhooks( + request: Request, + id: str, + user=Depends(get_verified_user), + db: Session = Depends(get_session), +): + check_channels_access(request) + channel = Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + # Only channel managers can view webhooks + if ( + not Channels.is_user_channel_manager(channel.id, user.id, db=db) + and user.role != "admin" + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.UNAUTHORIZED + ) + + return Channels.get_webhooks_by_channel_id(id, db=db) + + +@router.post("/{id}/webhooks/create", response_model=ChannelWebhookModel) +async def create_channel_webhook( + request: Request, + id: str, + form_data: ChannelWebhookForm, + user=Depends(get_verified_user), + db: Session = Depends(get_session), +): + check_channels_access(request) + channel = Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + # Only channel managers can create webhooks + if ( + not Channels.is_user_channel_manager(channel.id, user.id, db=db) + and user.role != "admin" + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.UNAUTHORIZED + ) + + webhook = Channels.insert_webhook(id, user.id, form_data, db=db) + if not webhook: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() + ) + + return webhook + + +@router.post("/{id}/webhooks/{webhook_id}/update", response_model=ChannelWebhookModel) +async def update_channel_webhook( + request: Request, + id: str, + webhook_id: str, + form_data: ChannelWebhookForm, + user=Depends(get_verified_user), + db: Session = Depends(get_session), +): + check_channels_access(request) + channel = Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + # Only channel managers can update webhooks + if ( + not Channels.is_user_channel_manager(channel.id, user.id, db=db) + and user.role != "admin" + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.UNAUTHORIZED + ) + + webhook = Channels.get_webhook_by_id(webhook_id, db=db) + if not webhook or webhook.channel_id != id: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + updated = Channels.update_webhook_by_id(webhook_id, form_data, db=db) + if not updated: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() + ) + + return updated + + +@router.delete("/{id}/webhooks/{webhook_id}/delete", response_model=bool) +async def delete_channel_webhook( + request: Request, + id: str, + webhook_id: str, + user=Depends(get_verified_user), + db: Session = Depends(get_session), +): + check_channels_access(request) + channel = Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + # Only channel managers can delete webhooks + if ( + not Channels.is_user_channel_manager(channel.id, user.id, db=db) + and user.role != "admin" + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.UNAUTHORIZED + ) + + webhook = Channels.get_webhook_by_id(webhook_id, db=db) + if not webhook or webhook.channel_id != id: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + return Channels.delete_webhook_by_id(webhook_id, db=db) + + +############################ +# Public Webhook Endpoint +############################ + + +class WebhookMessageForm(BaseModel): + content: str + + +@router.post("/webhooks/{webhook_id}/{token}") +async def post_webhook_message( + request: Request, + webhook_id: str, + token: str, + form_data: WebhookMessageForm, + db: Session = Depends(get_session), +): + """Public endpoint to post messages via webhook. No authentication required.""" + check_channels_access(request) + + # Validate webhook + webhook = Channels.get_webhook_by_id_and_token(webhook_id, token, db=db) + if not webhook: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid webhook URL", + ) + + channel = Channels.get_channel_by_id(webhook.channel_id, db=db) + if not channel: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + # Create message with webhook identity stored in meta + message = Messages.insert_new_message( + MessageForm(content=form_data.content, meta={"webhook": {"id": webhook.id}}), + webhook.channel_id, + webhook.user_id, # Required for DB but webhook info in meta takes precedence + db=db, + ) + + if not message: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Failed to create message", + ) + + # Update last_used_at + Channels.update_webhook_last_used_at(webhook_id, db=db) + + # Get full message and emit event + message = Messages.get_message_by_id(message.id, db=db) + + event_data = { + "channel_id": channel.id, + "message_id": message.id, + "data": { + "type": "message", + "data": { + **message.model_dump(), + "user": { + "id": webhook.id, + "name": webhook.name, + "role": "webhook", + }, + }, + }, + "user": { + "id": webhook.id, + "name": webhook.name, + "role": "webhook", + }, + "channel": channel.model_dump(), + } + + await sio.emit( + "events:channel", + event_data, + to=f"channel:{channel.id}", + ) + + return {"success": True, "message_id": message.id} diff --git a/src/lib/apis/channels/index.ts b/src/lib/apis/channels/index.ts index 44817e97ef..225d8cd7cf 100644 --- a/src/lib/apis/channels/index.ts +++ b/src/lib/apis/channels/index.ts @@ -763,3 +763,155 @@ export const deleteMessage = async (token: string = '', channel_id: string, mess return res; }; + +// Webhook API functions + +type WebhookForm = { + name: string; + profile_image_url?: string; +}; + +export const getChannelWebhooks = async (token: string = '', channel_id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/webhooks`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const createChannelWebhook = async ( + token: string = '', + channel_id: string, + formData: WebhookForm +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/webhooks/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ ...formData }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateChannelWebhook = async ( + token: string = '', + channel_id: string, + webhook_id: string, + formData: WebhookForm +) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/webhooks/${webhook_id}/update`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ ...formData }) + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteChannelWebhook = async ( + token: string = '', + channel_id: string, + webhook_id: string +) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/webhooks/${webhook_id}/delete`, + { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/components/channel/Channel.svelte b/src/lib/components/channel/Channel.svelte index 3229b65f90..3f650662ae 100644 --- a/src/lib/components/channel/Channel.svelte +++ b/src/lib/components/channel/Channel.svelte @@ -119,8 +119,8 @@ if (type === 'message') { if ((data?.parent_id ?? null) === null) { - const tempId = data?.temp_id ?? null; - messages = [{ ...data, temp_id: null }, ...messages.filter((m) => m?.temp_id !== tempId)]; + const tempId = data?.temp_id ?? null; + messages = [{ ...data, temp_id: null }, ...messages.filter((m) => !tempId || m?.temp_id !== tempId)]; if (typingUsers.find((user) => user.id === event.user.id)) { typingUsers = typingUsers.filter((user) => user.id !== event.user.id); diff --git a/src/lib/components/channel/Messages.svelte b/src/lib/components/channel/Messages.svelte index 7ad8798297..55f5df1238 100644 --- a/src/lib/components/channel/Messages.svelte +++ b/src/lib/components/channel/Messages.svelte @@ -131,8 +131,9 @@ replyToMessage={replyToMessage?.id === message.id} disabled={!channel?.write_access || message?.temp_id} pending={!!message?.temp_id} - showUserProfile={messageIdx === 0 || + showUserProfile={messageIdx === 0 || messageList.at(messageIdx - 1)?.user_id !== message.user_id || + messageList.at(messageIdx - 1)?.user?.id !== message.user?.id || messageList.at(messageIdx - 1)?.meta?.model_id !== message?.meta?.model_id || message?.reply_to_message !== null} onDelete={() => { diff --git a/src/lib/components/channel/Messages/Message.svelte b/src/lib/components/channel/Messages/Message.svelte index 9a636eabc9..72e81d9b95 100644 --- a/src/lib/components/channel/Messages/Message.svelte +++ b/src/lib/components/channel/Messages/Message.svelte @@ -245,7 +245,7 @@ /> {:else} {message.reply_to_message.user?.name @@ -277,10 +277,10 @@ alt={message.meta.model_name ?? message.meta.model_id} class="size-8 translate-y-1 ml-0.5 object-cover rounded-full" /> - {:else} + {:else} diff --git a/src/lib/components/channel/WebhookItem.svelte b/src/lib/components/channel/WebhookItem.svelte new file mode 100644 index 0000000000..6ee031f22f --- /dev/null +++ b/src/lib/components/channel/WebhookItem.svelte @@ -0,0 +1,164 @@ + + + + +
+ + + + + {#if expanded} +
+
+ +
+
{$i18n.t('Name')}
+ +
+
+ + + + + + +
+
+
+ {/if} +
diff --git a/src/lib/components/channel/WebhooksModal.svelte b/src/lib/components/channel/WebhooksModal.svelte new file mode 100644 index 0000000000..c095e38018 --- /dev/null +++ b/src/lib/components/channel/WebhooksModal.svelte @@ -0,0 +1,181 @@ + + + + +{#if channel} + +
+
+
+
+
{$i18n.t('Webhooks')}
+ {webhooks.length} +
+ + +
+ + +
+ +
+
{ + e.preventDefault(); + saveHandler(); + }} + > + {#if isLoading} +
+ +
+ {:else if webhooks.length > 0} +
+ {#each webhooks as webhook (webhook.id)} + { + selectedWebhookId = selectedWebhookId === webhook.id ? null : webhook.id; + }} + onDelete={() => { + showDeleteConfirmDialog = true; + }} + onUpdate={(changes) => { + pendingChanges[webhook.id] = changes; + }} + /> + {/each} +
+ {:else} +
+ {$i18n.t('No webhooks yet')} +
+ {/if} + +
+ +
+
+
+
+
+{/if} diff --git a/src/lib/components/layout/Sidebar/ChannelModal.svelte b/src/lib/components/layout/Sidebar/ChannelModal.svelte index b123989111..65404c056c 100644 --- a/src/lib/components/layout/Sidebar/ChannelModal.svelte +++ b/src/lib/components/layout/Sidebar/ChannelModal.svelte @@ -17,6 +17,7 @@ import MemberSelector from '$lib/components/workspace/common/MemberSelector.svelte'; import Visibility from '$lib/components/workspace/common/Visibility.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; + import WebhooksModal from '$lib/components/channel/WebhooksModal.svelte'; export let show = false; export let onSubmit: Function = () => {}; @@ -97,6 +98,7 @@ } let showDeleteConfirmDialog = false; + let showWebhooksModal = false; const deleteHandler = async () => { showDeleteConfirmDialog = false; @@ -126,7 +128,7 @@ }; - +
@@ -247,6 +249,22 @@
{/if} + {#if edit} +
+
{$i18n.t('Webhooks')}
+ + +
+ {/if} +
{#if edit}