(ci) try to pass more checks with sonarqube

This commit is contained in:
Thomas Ramé
2026-05-18 10:59:25 +02:00
parent 668f093063
commit e2a3b286ca
16 changed files with 273 additions and 240 deletions
+7 -6
View File
@@ -88,17 +88,18 @@ def generate_token(
str: The LiveKit JWT access token.
"""
if is_admin_or_owner:
sources = settings.LIVEKIT_DEFAULT_SOURCES
# Local import: core.models loads core.utils mid-import (see models.py:28),
# so importing EncryptionMode at module top would deadlock the bootstrap.
from core.models import EncryptionMode # pylint: disable=import-outside-toplevel
if sources is None:
if is_admin_or_owner or sources is None:
sources = settings.LIVEKIT_DEFAULT_SOURCES
# In encrypted rooms, no one can change their name/attributes after the
# admin accepted them — otherwise a participant authenticated under one
# identity could rewrite their JWT-presented name to spoof someone else
# mid-meeting. In plain rooms, free naming is fine.
can_update_own_metadata = encryption_mode == "none"
can_update_own_metadata = encryption_mode == EncryptionMode.NONE
video_grants = VideoGrants(
room=room,
@@ -136,7 +137,7 @@ def generate_token(
# otherwise.
if (
not user.is_anonymous
and encryption_mode != "none"
and encryption_mode != EncryptionMode.NONE
and getattr(user, "email", None)
):
attributes["email"] = user.email
@@ -159,7 +160,7 @@ def generate_token(
# that moment — no extra round-trip, no race window where a SIP
# caller could read empty metadata before the `room_started` webhook
# has time to push it.
if encryption_mode != "none":
if encryption_mode != EncryptionMode.NONE:
token = token.with_room_config(
RoomConfiguration(
name=room,
@@ -17,6 +17,7 @@ import { css } from '@/styled-system/css'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
import { ParticipantPlaceholder } from '@/features/rooms/livekit/components/ParticipantPlaceholder'
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
interface Props {
participant: Participant
@@ -28,7 +29,7 @@ export function DecryptionFailedTileOverlay({ participant }: Props) {
})
const room = useRoomContext()
const roomData = useRoomData()
const isEncrypted = roomData?.encryption_mode === 'basic'
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
const [failed, setFailed] = useState(false)
@@ -56,8 +57,7 @@ export function DecryptionFailedTileOverlay({ participant }: Props) {
if (!failed) return null
return (
<div
role="status"
<output
aria-label={t('title')}
className={css({
position: 'absolute',
@@ -108,6 +108,6 @@ export function DecryptionFailedTileOverlay({ participant }: Props) {
{t('body')}
</div>
</div>
</div>
</output>
)
}
@@ -20,6 +20,7 @@ import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
import { RecordingMode, useRecordingStatuses } from '@/features/recording'
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
const COLLAPSE_DELAY_MS = 4000
@@ -39,10 +40,9 @@ function StatusPill({ icon, label, background, pulse }: PillProps) {
}, [])
return (
<div
<output
onMouseEnter={() => setCollapsed(false)}
onMouseLeave={() => setCollapsed(true)}
role="status"
aria-label={label}
className={css({
display: 'inline-flex',
@@ -78,7 +78,7 @@ function StatusPill({ icon, label, background, pulse }: PillProps) {
>
{label}
</span>
</div>
</output>
)
}
@@ -95,7 +95,7 @@ export function RoomStatusBanner() {
const isRecording = screenRec.isStarted
const isTranscribing = transcript.isStarted
const isEncrypted = roomData?.encryption_mode === 'basic'
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
if (!isEncrypted && !isRecording && !isTranscribing) {
return null
@@ -7,6 +7,7 @@ import { isRoomValid } from '@/features/rooms'
import { normalizeRoomId } from '@/features/rooms/utils/isRoomValid'
import { fetchRoom } from '@/features/rooms/api/fetchRoom'
import { isValidPassphrase } from '@/features/encryption'
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
export const JoinMeetingDialog = () => {
const { t } = useTranslation('home')
@@ -39,7 +40,7 @@ export const JoinMeetingDialog = () => {
setIsLoading(true)
try {
const room = await fetchRoom({ roomId: parsed.roomId })
if (room.encryption_mode === 'basic') {
if (room.encryption_mode === ApiEncryptionMode.BASIC) {
setRoomId(parsed.roomId)
setStep('passphrase')
return
@@ -103,17 +103,19 @@ export const Conference = ({
const hasValidHash = isValidPassphrase(hashPassphrase)
const dbSaysEncrypted = data?.encryption_mode === ApiEncryptionMode.BASIC
const encryptionMismatch:
type EncryptionMismatch =
| 'missingPassphrase'
| 'unexpectedPassphrase'
| null =
data === undefined
? null
: dbSaysEncrypted && !hasValidHash
? 'missingPassphrase'
: !dbSaysEncrypted && hashPassphrase.length > 0
? 'unexpectedPassphrase'
: null
| null
let encryptionMismatch: EncryptionMismatch = null
if (data !== undefined) {
if (dbSaysEncrypted && !hasValidHash) {
encryptionMismatch = 'missingPassphrase'
} else if (!dbSaysEncrypted && hashPassphrase.length > 0) {
encryptionMismatch = 'unexpectedPassphrase'
}
}
// We treat the room as encrypted purely because we have a valid hash.
// No server condition. If the hash is valid we MUST run E2EE; if not,
@@ -17,7 +17,7 @@ import { useMemo } from 'react'
import { css } from '@/styled-system/css'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
import { FeaturePill } from '@/features/encryption'
import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom'
import { ApiAccessLevel, ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
import { useTelephony } from '@/features/rooms/livekit/hooks/useTelephony'
import { formatPinCode } from '@/features/rooms/utils/telephony'
import { useCopyRoomToClipboard } from '@/features/rooms/livekit/hooks/useCopyRoomToClipboard'
@@ -53,7 +53,7 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
})
const roomData = useRoomData()
const isEncrypted = roomData?.encryption_mode === 'basic'
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
const isAdminOrOwner = !!roomData?.is_administrable
const baseRoomUrl = getRouteUrl('room', roomData?.slug)
// Include the hash (passphrase) for basic encrypted rooms so the full link is visible
@@ -108,219 +108,230 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
) : (
<P>{t('description')}</P>
)}
{isEncrypted && !isAdminOrOwner ? null : isEncrypted ? (
<div
className={css({
width: '100%',
marginTop: '0.5rem',
display: 'flex',
flexDirection: 'column',
gap: '0.75rem',
})}
>
<div
role="alert"
className={css({
display: 'flex',
gap: '0.5rem',
alignItems: 'center',
padding: '0.6rem 0.85rem',
borderRadius: '0.5rem',
backgroundColor: '#fff7ed',
border: '1px solid #fed7aa',
color: '#7c2d12',
})}
>
<RiAlertFill
size={18}
color="#b45309"
className={css({ flexShrink: 0 })}
/>
<Text
variant="sm"
margin={false}
{(() => {
if (isEncrypted && !isAdminOrOwner) return null
if (isEncrypted) {
return (
<div
className={css({
color: '#7c2d12',
fontSize: '0.85rem',
lineHeight: 1.4,
width: '100%',
marginTop: '0.5rem',
display: 'flex',
flexDirection: 'column',
gap: '0.75rem',
})}
>
{tHome('warning')}
</Text>
</div>
<div
className={css({
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.5rem 0.75rem',
borderRadius: '0.5rem',
border: '1px solid',
borderColor: 'greyscale.250',
})}
>
<span
className={css({
flex: 1,
fontFamily: 'monospace',
fontSize: '0.8rem',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
})}
>
{roomUrl?.replace(/^https?:\/\//, '')}
</span>
<Button
variant={isRoomUrlCopied ? 'success' : 'tertiaryText'}
square
size="sm"
onPress={copyRoomUrlToClipboard}
aria-label={isRoomUrlCopied ? t('copied') : t('copyUrl')}
tooltip={isRoomUrlCopied ? t('copied') : t('copyUrl')}
>
{isRoomUrlCopied ? (
<RiCheckLine size={16} />
) : (
<RiFileCopyLine size={16} />
)}
</Button>
</div>
<Text
margin={false}
className={css({
fontSize: '12px',
fontWeight: 400,
color: 'greyscale.500',
})}
>
{t('encryptedDisabledHeading')}
</Text>
<div
className={css({
display: 'flex',
flexWrap: 'wrap',
gap: '0.4rem',
})}
>
<FeaturePill
size="sm"
icon={<RiPhoneLine size={13} />}
label={tFeatures('features.dialIn')}
/>
<FeaturePill
size="sm"
icon={<RiComputerLine size={13} />}
label={tFeatures('features.meetingRoom')}
/>
</div>
</div>
) : isTelephonyReadyForUse ? (
<div
className={css({
width: '100%',
display: 'flex',
flexDirection: 'column',
marginTop: '0.5rem',
gap: '1rem',
overflow: 'visible',
})}
>
<div
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
})}
>
<Text as="p" wrap="pretty">
{roomUrl?.replace(/^https?:\/\//, '')}
</Text>
{isTelephonyReadyForUse && roomUrl && (
<Button
variant={isRoomUrlCopied ? 'success' : 'tertiaryText'}
square
size={'sm'}
onPress={copyRoomUrlToClipboard}
aria-label={isRoomUrlCopied ? t('copied') : t('copyUrl')}
tooltip={isRoomUrlCopied ? t('copied') : t('copyUrl')}
<div
role="alert"
className={css({
display: 'flex',
gap: '0.5rem',
alignItems: 'center',
padding: '0.6rem 0.85rem',
borderRadius: '0.5rem',
backgroundColor: '#fff7ed',
border: '1px solid #fed7aa',
color: '#7c2d12',
})}
>
{isRoomUrlCopied ? (
<RiCheckLine aria-hidden="true" />
<RiAlertFill
size={18}
color="#b45309"
className={css({ flexShrink: 0 })}
/>
<Text
variant="sm"
margin={false}
className={css({
color: '#7c2d12',
fontSize: '0.85rem',
lineHeight: 1.4,
})}
>
{tHome('warning')}
</Text>
</div>
<div
className={css({
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.5rem 0.75rem',
borderRadius: '0.5rem',
border: '1px solid',
borderColor: 'greyscale.250',
})}
>
<span
className={css({
flex: 1,
fontFamily: 'monospace',
fontSize: '0.8rem',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
})}
>
{roomUrl?.replace(/^https?:\/\//, '')}
</span>
<Button
variant={isRoomUrlCopied ? 'success' : 'tertiaryText'}
square
size="sm"
onPress={copyRoomUrlToClipboard}
aria-label={isRoomUrlCopied ? t('copied') : t('copyUrl')}
tooltip={isRoomUrlCopied ? t('copied') : t('copyUrl')}
>
{isRoomUrlCopied ? (
<RiCheckLine size={16} />
) : (
<RiFileCopyLine size={16} />
)}
</Button>
</div>
<Text
margin={false}
className={css({
fontSize: '12px',
fontWeight: 400,
color: 'greyscale.500',
})}
>
{t('encryptedDisabledHeading')}
</Text>
<div
className={css({
display: 'flex',
flexWrap: 'wrap',
gap: '0.4rem',
})}
>
<FeaturePill
size="sm"
icon={<RiPhoneLine size={13} />}
label={tFeatures('features.dialIn')}
/>
<FeaturePill
size="sm"
icon={<RiComputerLine size={13} />}
label={tFeatures('features.meetingRoom')}
/>
</div>
</div>
)
}
if (isTelephonyReadyForUse) {
return (
<div
className={css({
width: '100%',
display: 'flex',
flexDirection: 'column',
marginTop: '0.5rem',
gap: '1rem',
overflow: 'visible',
})}
>
<div
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
})}
>
<Text as="p" wrap="pretty">
{roomUrl?.replace(/^https?:\/\//, '')}
</Text>
{isTelephonyReadyForUse && roomUrl && (
<Button
variant={isRoomUrlCopied ? 'success' : 'tertiaryText'}
square
size={'sm'}
onPress={copyRoomUrlToClipboard}
aria-label={
isRoomUrlCopied ? t('copied') : t('copyUrl')
}
tooltip={isRoomUrlCopied ? t('copied') : t('copyUrl')}
>
{isRoomUrlCopied ? (
<RiCheckLine aria-hidden="true" />
) : (
<RiFileCopyLine aria-hidden="true" />
)}
</Button>
)}
</div>
<div
className={css({
display: 'flex',
flexDirection: 'column',
})}
>
<Text as="p" wrap="pretty">
<Bold>{t('phone.call')}</Bold> ({telephony?.country}){' '}
{telephony?.internationalPhoneNumber}
</Text>
<Text as="p" wrap="pretty">
<Bold>{t('phone.pinCode')}</Bold>{' '}
{formatPinCode(roomData?.pin_code)}
</Text>
</div>
<Button
variant={isCopied ? 'success' : 'secondaryText'}
size="sm"
fullWidth
aria-label={isCopied ? t('copied') : t('copy')}
style={{
justifyContent: 'start',
}}
onPress={copyRoomToClipboard}
data-attr="share-dialog-copy"
>
{isCopied ? (
<>
<RiCheckLine
size={18}
style={{ marginRight: '8px' }}
aria-hidden="true"
/>
{t('copied')}
</>
) : (
<RiFileCopyLine aria-hidden="true" />
<>
<RiFileCopyLine
style={{ marginRight: '6px', minWidth: '18px' }}
aria-hidden="true"
/>
{t('copy')}
</>
)}
</Button>
)}
</div>
<div
className={css({
display: 'flex',
flexDirection: 'column',
})}
>
<Text as="p" wrap="pretty">
<Bold>{t('phone.call')}</Bold> ({telephony?.country}){' '}
{telephony?.internationalPhoneNumber}
</Text>
<Text as="p" wrap="pretty">
<Bold>{t('phone.pinCode')}</Bold>{' '}
{formatPinCode(roomData?.pin_code)}
</Text>
</div>
</div>
)
}
return (
<Button
variant={isCopied ? 'success' : 'secondaryText'}
size="sm"
variant={isCopied ? 'success' : 'tertiary'}
fullWidth
aria-label={isCopied ? t('copied') : t('copy')}
style={{
justifyContent: 'start',
}}
onPress={copyRoomToClipboard}
data-attr="share-dialog-copy"
>
{isCopied ? (
<>
<RiCheckLine
size={18}
style={{ marginRight: '8px' }}
aria-hidden="true"
/>
<RiCheckLine size={24} style={{ marginRight: '8px' }} />
{t('copied')}
</>
) : (
<>
<RiFileCopyLine
style={{ marginRight: '6px', minWidth: '18px' }}
aria-hidden="true"
/>
{t('copy')}
<RiFileCopyLine size={24} style={{ marginRight: '8px' }} />
{t('copyUrl')}
</>
)}
</Button>
</div>
) : (
<Button
variant={isCopied ? 'success' : 'tertiary'}
fullWidth
aria-label={isCopied ? t('copied') : t('copy')}
onPress={copyRoomToClipboard}
data-attr="share-dialog-copy"
>
{isCopied ? (
<>
<RiCheckLine size={24} style={{ marginRight: '8px' }} />
{t('copied')}
</>
) : (
<>
<RiFileCopyLine size={24} style={{ marginRight: '8px' }} />
{t('copyUrl')}
</>
)}
</Button>
)}
)
})()}
{roomData?.access_level === ApiAccessLevel.PUBLIC && (
<HStack>
<div
@@ -32,7 +32,7 @@ import { useQuery } from '@tanstack/react-query'
import { queryClient } from '@/api/queryClient'
import { ApiLobbyStatus, ApiRequestEntry } from '../api/requestEntry'
import { Spinner } from '@/primitives/Spinner'
import { ApiAccessLevel } from '../api/ApiRoom'
import { ApiAccessLevel, ApiEncryptionMode } from '../api/ApiRoom'
import {
isValidPassphrase,
getPassphraseFromHash,
@@ -118,7 +118,7 @@ export const Join = ({
retry: false,
})
const isEncryptedRoom = roomInfo?.encryption_mode === 'basic'
const isEncryptedRoom = roomInfo?.encryption_mode === ApiEncryptionMode.BASIC
const { user, isLoggedIn } = useUser()
// Authenticated joiners of an encrypted room can't pick an arbitrary
// display name — it must match the OIDC profile, and the backend
@@ -5,7 +5,7 @@ import { RiAlertFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { usePatchRoom } from '@/features/rooms/api/patchRoom'
import { fetchRoom } from '@/features/rooms/api/fetchRoom'
import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom'
import { ApiAccessLevel, ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
import { queryClient } from '@/api/queryClient'
import { keys } from '@/api/queryKeys'
import { useQuery } from '@tanstack/react-query'
@@ -168,7 +168,8 @@ export const Admin = () => {
{t('access.description')}
</Text>
{(() => {
const isEncrypted = readOnlyData?.encryption_mode === 'basic'
const isEncrypted =
readOnlyData?.encryption_mode === ApiEncryptionMode.BASIC
return (
<>
{isEncrypted && (
@@ -16,6 +16,7 @@ import { formatPinCode } from '../../utils/telephony'
import { useTelephony } from '../hooks/useTelephony'
import { useCopyRoomToClipboard } from '../hooks/useCopyRoomToClipboard'
import { FeaturePill } from '@/features/encryption'
import { ApiEncryptionMode } from '../../api/ApiRoom'
export const Info = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'info' })
@@ -30,7 +31,7 @@ export const Info = () => {
: baseRoomUrl
const telephony = useTelephony()
const isEncrypted = data?.encryption_mode === 'basic'
const isEncrypted = data?.encryption_mode === ApiEncryptionMode.BASIC
const isAdminOrOwner = !!data?.is_administrable
const isTelephonyReadyForUse = useMemo(() => {
@@ -14,6 +14,7 @@ import {
} from '@/features/recording'
import { useConfig } from '@/api/useConfig'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
export interface ToolsButtonProps {
icon: ReactNode
@@ -108,7 +109,7 @@ export const Tools = () => {
useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' })
const roomData = useRoomData()
const isEncrypted = roomData?.encryption_mode === 'basic'
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
// Restore focus to the element that opened the Tools panel
useRestoreFocus(isToolsOpen, {
@@ -6,13 +6,14 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
import { RecordingMode, useHasRecordingAccess } from '@/features/recording'
import { FeatureFlags } from '@/features/analytics/enums'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
export const ScreenRecordingMenuItem = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
const { isScreenRecordingOpen, openScreenRecording, toggleTools } =
useSidePanel()
const roomData = useRoomData()
const isEncrypted = roomData?.encryption_mode === 'basic'
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
const hasScreenRecordingAccess = useHasRecordingAccess(
RecordingMode.ScreenRecording,
@@ -6,12 +6,13 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
import { RecordingMode, useHasRecordingAccess } from '@/features/recording'
import { FeatureFlags } from '@/features/analytics/enums'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
export const TranscriptMenuItem = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
const { isTranscriptOpen, openTranscript, toggleTools } = useSidePanel()
const roomData = useRoomData()
const isEncrypted = roomData?.encryption_mode === 'basic'
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
const hasTranscriptAccess = useHasRecordingAccess(
RecordingMode.Transcript,
@@ -2,7 +2,7 @@ import { useTelephony } from './useTelephony'
import { useTranslation } from 'react-i18next'
import { useEffect, useMemo, useState } from 'react'
import { formatPinCode } from '@/features/rooms/utils/telephony'
import { ApiRoom } from '@/features/rooms/api/ApiRoom'
import { ApiRoom, ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
import { getRouteUrl } from '@/navigation/getRouteUrl'
const COPY_SUCCESS_TIMEOUT = 3000
@@ -47,7 +47,9 @@ export const useCopyRoomToClipboard = (
// so make sure we don't paste a non-functional phone+PIN snippet either.
const hasTelephonyInfo = useMemo(() => {
return (
telephony.enabled && room?.pin_code && room?.encryption_mode !== 'basic'
telephony.enabled &&
room?.pin_code &&
room?.encryption_mode !== ApiEncryptionMode.BASIC
)
}, [telephony.enabled, room?.pin_code, room?.encryption_mode])
@@ -77,10 +77,17 @@ export const CreateMeetingButton = () => {
// Communicate iframe height to parent for proper sizing
useEffect(() => {
const targetOrigin = (() => {
try {
return document.referrer ? new URL(document.referrer).origin : '/'
} catch {
return '/'
}
})()
const observer = new ResizeObserver(() => {
window.parent.postMessage(
{ type: 'RESIZE', data: { height: document.body.scrollHeight } },
'*'
targetOrigin
)
})
observer.observe(document.body)
@@ -30,16 +30,21 @@ export const EncryptionDefaultField = () => {
const isOn = user?.default_encryption_mode === ApiEncryptionMode.BASIC
const handleToggle = (next: boolean) => {
const handleToggle = async (next: boolean) => {
if (!user) return
void mutateAsync({
user: {
id: user.id,
default_encryption_mode: next
? ApiEncryptionMode.BASIC
: ApiEncryptionMode.NONE,
},
})
try {
await mutateAsync({
user: {
id: user.id,
default_encryption_mode: next
? ApiEncryptionMode.BASIC
: ApiEncryptionMode.NONE,
},
})
} catch {
// react-query stores the error on the mutation; the UI shows
// isPending while in flight and reverts the toggle on failure.
}
}
if (!isLoggedIn) {
+2 -3
View File
@@ -20,8 +20,7 @@ export const navigateTo = <S = unknown>(
// Including the hash in the URL passed to `navigate` lets us avoid a
// brittle pushState + replaceState dance at the call site: the URL the
// app first renders already carries the fragment.
const target = options?.hash ? `${to}#${options.hash}` : to
const { hash: _hash, ...navigateOptions } = options ?? {}
void _hash
const { hash, ...navigateOptions } = options ?? {}
const target = hash ? `${to}#${hash}` : to
return navigate(target, navigateOptions)
}