From d823ebd96d2996e2d8a5cd4cf5596c5e8b8db1fd Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 12 Oct 2026 23:59:19 +0400 Subject: [PATCH] refac --- backend/open_webui/models/apps.py | 21 ++++++++++++++++++- backend/open_webui/models/messages.py | 7 ++++++- src/lib/apis/apps/index.ts | 1 + src/lib/components/admin/Settings/Apps.svelte | 9 +++++++- .../channel/ChannelInfoModal/UserList.svelte | 9 +++++++- .../channel/MessageInput/MentionList.svelte | 18 +++++++++++++--- .../channel/Messages/Message.svelte | 11 +++++++++- .../chat/Messages/ProfileImage.svelte | 3 ++- .../workspace/common/MemberSelector.svelte | 9 +++++++- 9 files changed, 78 insertions(+), 10 deletions(-) diff --git a/backend/open_webui/models/apps.py b/backend/open_webui/models/apps.py index 2b118c2a75..4329fe5cc9 100644 --- a/backend/open_webui/models/apps.py +++ b/backend/open_webui/models/apps.py @@ -1,12 +1,14 @@ """External apps. Credentials never leave the internal model.""" +import base64 +import re import time from typing import Literal from open_webui.internal.db import Base, get_async_db_context from open_webui.models.access_grants import AccessGrants from open_webui.models.channels import ChannelMember -from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator +from pydantic import BaseModel, ConfigDict, Field, HttpUrl, SecretStr, field_validator from sqlalchemy import JSON, BigInteger, Boolean, Column, Text, select, update from sqlalchemy.ext.asyncio import AsyncSession @@ -54,9 +56,26 @@ class Manifest(StrictModel): id: str = Field(min_length=1, max_length=100) name: str = Field(min_length=1, max_length=100) description: str = Field(default='', max_length=2000) + profile_image_url: str | None = Field(default=None, max_length=196608) public_key: str = Field(max_length=2000) requested_scopes: list[Scope] + @field_validator('profile_image_url') + @classmethod + def validate_profile_image_url(cls, value): + if value is not None: + if value.startswith('data:'): + header, encoded = value.split(',', 1) + if not re.fullmatch(r'data:image/(png|jpeg|gif|webp);base64', header): + raise ValueError('Expected a base64 PNG, JPEG, GIF or WebP data URL') + if not base64.b64decode(encoded, validate=True): + raise ValueError('Profile image data must not be empty') + else: + url = HttpUrl(value) + if len(value) > 2048 or url.username is not None or url.password is not None: + raise ValueError('Profile image URL must not contain credentials or exceed 2048 characters') + return value + @field_validator('public_key') @classmethod def validate_public_key(cls, value): diff --git a/backend/open_webui/models/messages.py b/backend/open_webui/models/messages.py index 12aa9ac969..8b031404e6 100644 --- a/backend/open_webui/models/messages.py +++ b/backend/open_webui/models/messages.py @@ -295,7 +295,12 @@ class MessageTable: if message.app_id: from open_webui.models.apps import App app = await db.get(App, message.app_id) - return {'id': message.app_id, 'name': app.name if app else 'Removed App', 'role': 'app'} + return { + 'id': message.app_id, + 'name': app.name if app else 'Removed App', + 'role': 'app', + 'profile_image_url': app.data.get('manifest', {}).get('profile_image_url') if app else None, + } webhook_info = message.meta.get('webhook') if message.meta else None if webhook_info and webhook_info.get('id'): webhook = await Channels.get_webhook_by_id(webhook_info.get('id'), db=db) diff --git a/src/lib/apis/apps/index.ts b/src/lib/apis/apps/index.ts index 5e7c5f6c05..e86e9724a9 100644 --- a/src/lib/apis/apps/index.ts +++ b/src/lib/apis/apps/index.ts @@ -10,6 +10,7 @@ export type App = { url: string; manifest: { description: string; + profile_image_url?: string | null; }; granted_scopes: string[]; }; diff --git a/src/lib/components/admin/Settings/Apps.svelte b/src/lib/components/admin/Settings/Apps.svelte index 120daed4b5..bf51ae47bd 100644 --- a/src/lib/components/admin/Settings/Apps.svelte +++ b/src/lib/components/admin/Settings/Apps.svelte @@ -1,4 +1,5 @@ + import ProfileImage from '$lib/components/chat/Messages/ProfileImage.svelte'; import { toast } from 'svelte-sonner'; import { getContext, onMount, onDestroy } from 'svelte'; @@ -357,7 +358,13 @@
- + {#if app.data.manifest.profile_image_url} + + {:else}{/if}
{app.name}