This commit is contained in:
Timothy Jaeryang Baek
2026-04-19 22:22:15 +09:00
parent 98627e42b4
commit f45d0f130e
7 changed files with 63 additions and 14 deletions
+6
View File
@@ -1624,6 +1624,12 @@ ENABLE_CHANNELS = PersistentConfig(
os.environ.get('ENABLE_CHANNELS', 'False').lower() == 'true', os.environ.get('ENABLE_CHANNELS', 'False').lower() == 'true',
) )
ENABLE_CALENDAR = PersistentConfig(
'ENABLE_CALENDAR',
'calendar.enable',
os.environ.get('ENABLE_CALENDAR', 'True').lower() == 'true',
)
AUTOMATION_MAX_COUNT = PersistentConfig( AUTOMATION_MAX_COUNT = PersistentConfig(
'AUTOMATION_MAX_COUNT', 'AUTOMATION_MAX_COUNT',
'automations.max_count', 'automations.max_count',
+3
View File
@@ -396,6 +396,7 @@ from open_webui.config import (
AUTOMATION_MAX_COUNT, AUTOMATION_MAX_COUNT,
AUTOMATION_MIN_INTERVAL, AUTOMATION_MIN_INTERVAL,
ENABLE_CHANNELS, ENABLE_CHANNELS,
ENABLE_CALENDAR,
ENABLE_NOTES, ENABLE_NOTES,
ENABLE_USER_STATUS, ENABLE_USER_STATUS,
ENABLE_COMMUNITY_SHARING, ENABLE_COMMUNITY_SHARING,
@@ -902,6 +903,7 @@ app.state.config.FOLDER_MAX_FILE_COUNT = FOLDER_MAX_FILE_COUNT
app.state.config.AUTOMATION_MAX_COUNT = AUTOMATION_MAX_COUNT app.state.config.AUTOMATION_MAX_COUNT = AUTOMATION_MAX_COUNT
app.state.config.AUTOMATION_MIN_INTERVAL = AUTOMATION_MIN_INTERVAL app.state.config.AUTOMATION_MIN_INTERVAL = AUTOMATION_MIN_INTERVAL
app.state.config.ENABLE_CHANNELS = ENABLE_CHANNELS app.state.config.ENABLE_CHANNELS = ENABLE_CHANNELS
app.state.config.ENABLE_CALENDAR = ENABLE_CALENDAR
app.state.config.ENABLE_NOTES = ENABLE_NOTES app.state.config.ENABLE_NOTES = ENABLE_NOTES
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING
@@ -2218,6 +2220,7 @@ async def get_app_config(request: Request):
'enable_folders': app.state.config.ENABLE_FOLDERS, 'enable_folders': app.state.config.ENABLE_FOLDERS,
'folder_max_file_count': app.state.config.FOLDER_MAX_FILE_COUNT, 'folder_max_file_count': app.state.config.FOLDER_MAX_FILE_COUNT,
'enable_channels': app.state.config.ENABLE_CHANNELS, 'enable_channels': app.state.config.ENABLE_CHANNELS,
'enable_calendar': app.state.config.ENABLE_CALENDAR,
'enable_notes': app.state.config.ENABLE_NOTES, 'enable_notes': app.state.config.ENABLE_NOTES,
'enable_web_search': app.state.config.ENABLE_WEB_SEARCH, 'enable_web_search': app.state.config.ENABLE_WEB_SEARCH,
'enable_code_execution': app.state.config.ENABLE_CODE_EXECUTION, 'enable_code_execution': app.state.config.ENABLE_CODE_EXECUTION,
+4
View File
@@ -972,6 +972,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT, 'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT,
'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL, 'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL,
'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS, 'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS,
'ENABLE_CALENDAR': request.app.state.config.ENABLE_CALENDAR,
'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES, 'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES,
'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES, 'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES,
'ENABLE_USER_WEBHOOKS': request.app.state.config.ENABLE_USER_WEBHOOKS, 'ENABLE_USER_WEBHOOKS': request.app.state.config.ENABLE_USER_WEBHOOKS,
@@ -1000,6 +1001,7 @@ class AdminConfig(BaseModel):
AUTOMATION_MAX_COUNT: Optional[int | str] = None AUTOMATION_MAX_COUNT: Optional[int | str] = None
AUTOMATION_MIN_INTERVAL: Optional[int | str] = None AUTOMATION_MIN_INTERVAL: Optional[int | str] = None
ENABLE_CHANNELS: bool ENABLE_CHANNELS: bool
ENABLE_CALENDAR: bool
ENABLE_MEMORIES: bool ENABLE_MEMORIES: bool
ENABLE_NOTES: bool ENABLE_NOTES: bool
ENABLE_USER_WEBHOOKS: bool ENABLE_USER_WEBHOOKS: bool
@@ -1031,6 +1033,7 @@ async def update_admin_config(request: Request, form_data: AdminConfig, user=Dep
int(form_data.AUTOMATION_MIN_INTERVAL) if form_data.AUTOMATION_MIN_INTERVAL else '' int(form_data.AUTOMATION_MIN_INTERVAL) if form_data.AUTOMATION_MIN_INTERVAL else ''
) )
request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS
request.app.state.config.ENABLE_CALENDAR = form_data.ENABLE_CALENDAR
request.app.state.config.ENABLE_MEMORIES = form_data.ENABLE_MEMORIES request.app.state.config.ENABLE_MEMORIES = form_data.ENABLE_MEMORIES
request.app.state.config.ENABLE_NOTES = form_data.ENABLE_NOTES request.app.state.config.ENABLE_NOTES = form_data.ENABLE_NOTES
@@ -1074,6 +1077,7 @@ async def update_admin_config(request: Request, form_data: AdminConfig, user=Dep
'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT, 'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT,
'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL, 'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL,
'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS, 'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS,
'ENABLE_CALENDAR': request.app.state.config.ENABLE_CALENDAR,
'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES, 'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES,
'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES, 'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES,
'ENABLE_USER_WEBHOOKS': request.app.state.config.ENABLE_USER_WEBHOOKS, 'ENABLE_USER_WEBHOOKS': request.app.state.config.ENABLE_USER_WEBHOOKS,
+37 -12
View File
@@ -2,7 +2,7 @@ import logging
import time import time
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel from pydantic import BaseModel
from open_webui.models.calendar import ( from open_webui.models.calendar import (
@@ -24,12 +24,22 @@ from open_webui.models.groups import Groups
from open_webui.models.users import UserModel from open_webui.models.users import UserModel
from open_webui.utils.auth import get_verified_user from open_webui.utils.auth import get_verified_user
from open_webui.utils.calendar import expand_recurring_event from open_webui.utils.calendar import expand_recurring_event
from open_webui.constants import ERROR_MESSAGES
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
async def check_calendar_enabled(request: Request):
"""Dependency to ensure calendar feature is globally enabled."""
if not request.app.state.config.ENABLE_CALENDAR:
raise HTTPException(
status_code=403,
detail=ERROR_MESSAGES.FEATURE_DISABLED('Calendar'),
)
async def _check_calendar_access( async def _check_calendar_access(
calendar_id: str, user: UserModel, permission: str = 'write' calendar_id: str, user: UserModel, permission: str = 'write'
) -> CalendarModel: ) -> CalendarModel:
@@ -58,14 +68,16 @@ async def _check_calendar_access(
@router.get('/', response_model=list[CalendarModel]) @router.get('/', response_model=list[CalendarModel])
async def get_calendars(user: UserModel = Depends(get_verified_user)): async def get_calendars(request: Request, user: UserModel = Depends(get_verified_user)):
"""List user's calendars (owned + shared). Auto-creates defaults on first call.""" """List user's calendars (owned + shared). Auto-creates defaults on first call."""
await check_calendar_enabled(request)
return await Calendars.get_calendars_by_user(user.id) return await Calendars.get_calendars_by_user(user.id)
@router.post('/create', response_model=CalendarModel) @router.post('/create', response_model=CalendarModel)
async def create_calendar(form_data: CalendarForm, user: UserModel = Depends(get_verified_user)): async def create_calendar(request: Request, form_data: CalendarForm, user: UserModel = Depends(get_verified_user)):
"""Create a new user calendar.""" """Create a new user calendar."""
await check_calendar_enabled(request)
return await Calendars.insert_new_calendar(user.id, form_data) return await Calendars.insert_new_calendar(user.id, form_data)
@@ -76,6 +88,7 @@ async def create_calendar(form_data: CalendarForm, user: UserModel = Depends(get
@router.get('/events') @router.get('/events')
async def get_events( async def get_events(
request: Request,
start: str, start: str,
end: str, end: str,
calendar_ids: Optional[str] = None, calendar_ids: Optional[str] = None,
@@ -92,6 +105,7 @@ async def get_events(
- Stored events from the database - Stored events from the database
- Virtual events computed from active automation RRULEs (Scheduled Tasks calendar) - Virtual events computed from active automation RRULEs (Scheduled Tasks calendar)
""" """
await check_calendar_enabled(request)
from datetime import datetime from datetime import datetime
try: try:
@@ -203,25 +217,29 @@ async def get_events(
@router.post('/events/create', response_model=CalendarEventModel) @router.post('/events/create', response_model=CalendarEventModel)
async def create_event(form_data: CalendarEventForm, user: UserModel = Depends(get_verified_user)): async def create_event(request: Request, form_data: CalendarEventForm, user: UserModel = Depends(get_verified_user)):
await check_calendar_enabled(request)
await _check_calendar_access(form_data.calendar_id, user, 'write') await _check_calendar_access(form_data.calendar_id, user, 'write')
return await CalendarEvents.insert_new_event(user.id, form_data) return await CalendarEvents.insert_new_event(user.id, form_data)
@router.get('/events/search', response_model=CalendarEventListResponse) @router.get('/events/search', response_model=CalendarEventListResponse)
async def search_events( async def search_events(
request: Request,
query: Optional[str] = None, query: Optional[str] = None,
skip: int = 0, skip: int = 0,
limit: int = 30, limit: int = 30,
user: UserModel = Depends(get_verified_user), user: UserModel = Depends(get_verified_user),
): ):
await check_calendar_enabled(request)
return await CalendarEvents.search_events( return await CalendarEvents.search_events(
user_id=user.id, query=query, skip=skip, limit=limit user_id=user.id, query=query, skip=skip, limit=limit
) )
@router.get('/events/{event_id}', response_model=CalendarEventModel) @router.get('/events/{event_id}', response_model=CalendarEventModel)
async def get_event(event_id: str, user: UserModel = Depends(get_verified_user)): async def get_event(request: Request, event_id: str, user: UserModel = Depends(get_verified_user)):
await check_calendar_enabled(request)
event = await CalendarEvents.get_event_by_id(event_id) event = await CalendarEvents.get_event_by_id(event_id)
if not event: if not event:
raise HTTPException(status_code=404, detail='Event not found') raise HTTPException(status_code=404, detail='Event not found')
@@ -233,8 +251,9 @@ async def get_event(event_id: str, user: UserModel = Depends(get_verified_user))
@router.post('/events/{event_id}/update', response_model=CalendarEventModel) @router.post('/events/{event_id}/update', response_model=CalendarEventModel)
async def update_event( async def update_event(
event_id: str, form_data: CalendarEventUpdateForm, user: UserModel = Depends(get_verified_user) request: Request, event_id: str, form_data: CalendarEventUpdateForm, user: UserModel = Depends(get_verified_user)
): ):
await check_calendar_enabled(request)
event = await CalendarEvents.get_event_by_id(event_id) event = await CalendarEvents.get_event_by_id(event_id)
if not event: if not event:
raise HTTPException(status_code=404, detail='Event not found') raise HTTPException(status_code=404, detail='Event not found')
@@ -248,7 +267,8 @@ async def update_event(
@router.delete('/events/{event_id}/delete') @router.delete('/events/{event_id}/delete')
async def delete_event(event_id: str, user: UserModel = Depends(get_verified_user)): async def delete_event(request: Request, event_id: str, user: UserModel = Depends(get_verified_user)):
await check_calendar_enabled(request)
event = await CalendarEvents.get_event_by_id(event_id) event = await CalendarEvents.get_event_by_id(event_id)
if not event: if not event:
raise HTTPException(status_code=404, detail='Event not found') raise HTTPException(status_code=404, detail='Event not found')
@@ -263,9 +283,10 @@ async def delete_event(event_id: str, user: UserModel = Depends(get_verified_use
@router.post('/events/{event_id}/rsvp', response_model=dict) @router.post('/events/{event_id}/rsvp', response_model=dict)
async def rsvp_event( async def rsvp_event(
event_id: str, form_data: RSVPForm, user: UserModel = Depends(get_verified_user) request: Request, event_id: str, form_data: RSVPForm, user: UserModel = Depends(get_verified_user)
): ):
"""Update own RSVP status for an event.""" """Update own RSVP status for an event."""
await check_calendar_enabled(request)
if form_data.status not in ('accepted', 'declined', 'tentative', 'pending'): if form_data.status not in ('accepted', 'declined', 'tentative', 'pending'):
raise HTTPException(status_code=400, detail='Invalid status') raise HTTPException(status_code=400, detail='Invalid status')
@@ -281,15 +302,17 @@ async def rsvp_event(
@router.get('/{calendar_id}', response_model=CalendarModel) @router.get('/{calendar_id}', response_model=CalendarModel)
async def get_calendar_by_id(calendar_id: str, user: UserModel = Depends(get_verified_user)): async def get_calendar_by_id(request: Request, calendar_id: str, user: UserModel = Depends(get_verified_user)):
await check_calendar_enabled(request)
cal = await _check_calendar_access(calendar_id, user, 'read') cal = await _check_calendar_access(calendar_id, user, 'read')
return cal return cal
@router.post('/{calendar_id}/update', response_model=CalendarModel) @router.post('/{calendar_id}/update', response_model=CalendarModel)
async def update_calendar( async def update_calendar(
calendar_id: str, form_data: CalendarUpdateForm, user: UserModel = Depends(get_verified_user) request: Request, calendar_id: str, form_data: CalendarUpdateForm, user: UserModel = Depends(get_verified_user)
): ):
await check_calendar_enabled(request)
cal = await _check_calendar_access(calendar_id, user, 'write') cal = await _check_calendar_access(calendar_id, user, 'write')
# Only owner/admin can change access grants # Only owner/admin can change access grants
@@ -303,7 +326,8 @@ async def update_calendar(
@router.delete('/{calendar_id}/delete') @router.delete('/{calendar_id}/delete')
async def delete_calendar(calendar_id: str, user: UserModel = Depends(get_verified_user)): async def delete_calendar(request: Request, calendar_id: str, user: UserModel = Depends(get_verified_user)):
await check_calendar_enabled(request)
cal = await _check_calendar_access(calendar_id, user, 'write') cal = await _check_calendar_access(calendar_id, user, 'write')
# Only owner/admin can delete # Only owner/admin can delete
@@ -318,7 +342,8 @@ async def delete_calendar(calendar_id: str, user: UserModel = Depends(get_verifi
@router.post('/{calendar_id}/default') @router.post('/{calendar_id}/default')
async def set_default_calendar(calendar_id: str, user: UserModel = Depends(get_verified_user)): async def set_default_calendar(request: Request, calendar_id: str, user: UserModel = Depends(get_verified_user)):
await check_calendar_enabled(request)
cal = await Calendars.set_default_calendar(user.id, calendar_id) cal = await Calendars.set_default_calendar(user.id, calendar_id)
if not cal: if not cal:
raise HTTPException(status_code=404, detail='Calendar not found') raise HTTPException(status_code=404, detail='Calendar not found')
+4 -1
View File
@@ -557,7 +557,10 @@ async def get_builtin_tools(
) )
# Calendar tools - search/create/update/delete events # Calendar tools - search/create/update/delete events
if is_builtin_tool_enabled('calendar'): if (
is_builtin_tool_enabled('calendar')
and getattr(request.app.state.config, 'ENABLE_CALENDAR', False)
):
builtin_functions.extend( builtin_functions.extend(
[search_calendar_events, create_calendar_event, update_calendar_event, delete_calendar_event] [search_calendar_events, create_calendar_event, update_calendar_event, delete_calendar_event]
) )
@@ -756,6 +756,14 @@
<Switch bind:state={adminConfig.ENABLE_CHANNELS} /> <Switch bind:state={adminConfig.ENABLE_CHANNELS} />
</div> </div>
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-medium">
{$i18n.t('Calendar')} ({$i18n.t('Beta')})
</div>
<Switch bind:state={adminConfig.ENABLE_CALENDAR} />
</div>
<div class="mb-2.5 flex w-full items-center justify-between pr-2"> <div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Memories')} ({$i18n.t('Beta')}) {$i18n.t('Memories')} ({$i18n.t('Beta')})
@@ -250,7 +250,7 @@
</a> </a>
{/if} {/if}
{#if $user?.role === 'admin' || $user?.permissions?.features?.calendar} {#if $config?.features?.enable_calendar && ($user?.role === 'admin' || $user?.permissions?.features?.calendar)}
<a <a
href="/calendar" href="/calendar"
draggable="false" draggable="false"