mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-21 04:39:06 +00:00
Merge branch 'main' into mathieu/calendar-integration-rebased
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.11.0](https://github.com/Monadical-SAS/reflector/compare/v0.10.0...v0.11.0) (2025-09-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* remove profanity filter that was there for conference ([#652](https://github.com/Monadical-SAS/reflector/issues/652)) ([b42f7cf](https://github.com/Monadical-SAS/reflector/commit/b42f7cfc606783afcee792590efcc78b507468ab))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* zulip and consent handler on the file pipeline ([#645](https://github.com/Monadical-SAS/reflector/issues/645)) ([5f143fe](https://github.com/Monadical-SAS/reflector/commit/5f143fe3640875dcb56c26694254a93189281d17))
|
||||||
|
* zulip stream and topic selection in share dialog ([#644](https://github.com/Monadical-SAS/reflector/issues/644)) ([c546e69](https://github.com/Monadical-SAS/reflector/commit/c546e69739e68bb74fbc877eb62609928e5b8de6))
|
||||||
|
|
||||||
## [0.10.0](https://github.com/Monadical-SAS/reflector/compare/v0.9.0...v0.10.0) (2025-09-11)
|
## [0.10.0](https://github.com/Monadical-SAS/reflector/compare/v0.9.0...v0.10.0) (2025-09-11)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ dependencies = [
|
|||||||
"prometheus-fastapi-instrumentator>=6.1.0",
|
"prometheus-fastapi-instrumentator>=6.1.0",
|
||||||
"sentencepiece>=0.1.99",
|
"sentencepiece>=0.1.99",
|
||||||
"protobuf>=4.24.3",
|
"protobuf>=4.24.3",
|
||||||
"profanityfilter>=2.0.6",
|
|
||||||
"celery>=5.3.4",
|
"celery>=5.3.4",
|
||||||
"redis>=5.0.1",
|
"redis>=5.0.1",
|
||||||
"python-jose[cryptography]>=3.3.0",
|
"python-jose[cryptography]>=3.3.0",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import av
|
import av
|
||||||
import structlog
|
import structlog
|
||||||
from celery import shared_task
|
from celery import chain, shared_task
|
||||||
|
|
||||||
from reflector.asynctask import asynctask
|
from reflector.asynctask import asynctask
|
||||||
from reflector.db.rooms import rooms_controller
|
from reflector.db.rooms import rooms_controller
|
||||||
@@ -26,6 +26,8 @@ from reflector.logger import logger
|
|||||||
from reflector.pipelines.main_live_pipeline import (
|
from reflector.pipelines.main_live_pipeline import (
|
||||||
PipelineMainBase,
|
PipelineMainBase,
|
||||||
broadcast_to_sockets,
|
broadcast_to_sockets,
|
||||||
|
task_cleanup_consent,
|
||||||
|
task_pipeline_post_to_zulip,
|
||||||
)
|
)
|
||||||
from reflector.processors import (
|
from reflector.processors import (
|
||||||
AudioFileWriterProcessor,
|
AudioFileWriterProcessor,
|
||||||
@@ -379,6 +381,28 @@ class PipelineMainFile(PipelineMainBase):
|
|||||||
await processor.flush()
|
await processor.flush()
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
@asynctask
|
||||||
|
async def task_send_webhook_if_needed(*, transcript_id: str):
|
||||||
|
"""Send webhook if this is a room recording with webhook configured"""
|
||||||
|
transcript = await transcripts_controller.get_by_id(transcript_id)
|
||||||
|
if not transcript:
|
||||||
|
return
|
||||||
|
|
||||||
|
if transcript.source_kind == SourceKind.ROOM and transcript.room_id:
|
||||||
|
room = await rooms_controller.get_by_id(transcript.room_id)
|
||||||
|
if room and room.webhook_url:
|
||||||
|
logger.info(
|
||||||
|
"Dispatching webhook",
|
||||||
|
transcript_id=transcript_id,
|
||||||
|
room_id=room.id,
|
||||||
|
webhook_url=room.webhook_url,
|
||||||
|
)
|
||||||
|
send_transcript_webhook.delay(
|
||||||
|
transcript_id, room.id, event_id=uuid.uuid4().hex
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
@asynctask
|
@asynctask
|
||||||
async def task_pipeline_file_process(*, transcript_id: str):
|
async def task_pipeline_file_process(*, transcript_id: str):
|
||||||
@@ -406,16 +430,10 @@ async def task_pipeline_file_process(*, transcript_id: str):
|
|||||||
await pipeline.set_status(transcript_id, "error")
|
await pipeline.set_status(transcript_id, "error")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# Trigger webhook if this is a room recording with webhook configured
|
# Run post-processing chain: consent cleanup -> zulip -> webhook
|
||||||
if transcript.source_kind == SourceKind.ROOM and transcript.room_id:
|
post_chain = chain(
|
||||||
room = await rooms_controller.get_by_id(transcript.room_id)
|
task_cleanup_consent.si(transcript_id=transcript_id),
|
||||||
if room and room.webhook_url:
|
task_pipeline_post_to_zulip.si(transcript_id=transcript_id),
|
||||||
logger.info(
|
task_send_webhook_if_needed.si(transcript_id=transcript_id),
|
||||||
"Dispatching webhook task",
|
)
|
||||||
transcript_id=transcript_id,
|
post_chain.delay()
|
||||||
room_id=room.id,
|
|
||||||
webhook_url=room.webhook_url,
|
|
||||||
)
|
|
||||||
send_transcript_webhook.delay(
|
|
||||||
transcript_id, room.id, event_id=uuid.uuid4().hex
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -4,11 +4,8 @@ import tempfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Annotated, TypedDict
|
from typing import Annotated, TypedDict
|
||||||
|
|
||||||
from profanityfilter import ProfanityFilter
|
|
||||||
from pydantic import BaseModel, Field, PrivateAttr
|
from pydantic import BaseModel, Field, PrivateAttr
|
||||||
|
|
||||||
from reflector.redis_cache import redis_cache
|
|
||||||
|
|
||||||
|
|
||||||
class DiarizationSegment(TypedDict):
|
class DiarizationSegment(TypedDict):
|
||||||
"""Type definition for diarization segment containing speaker information"""
|
"""Type definition for diarization segment containing speaker information"""
|
||||||
@@ -20,9 +17,6 @@ class DiarizationSegment(TypedDict):
|
|||||||
|
|
||||||
PUNC_RE = re.compile(r"[.;:?!…]")
|
PUNC_RE = re.compile(r"[.;:?!…]")
|
||||||
|
|
||||||
profanity_filter = ProfanityFilter()
|
|
||||||
profanity_filter.set_censor("*")
|
|
||||||
|
|
||||||
|
|
||||||
class AudioFile(BaseModel):
|
class AudioFile(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
@@ -124,21 +118,11 @@ def words_to_segments(words: list[Word]) -> list[TranscriptSegment]:
|
|||||||
|
|
||||||
class Transcript(BaseModel):
|
class Transcript(BaseModel):
|
||||||
translation: str | None = None
|
translation: str | None = None
|
||||||
words: list[Word] = None
|
words: list[Word] = []
|
||||||
|
|
||||||
@property
|
|
||||||
def raw_text(self):
|
|
||||||
# Uncensored text
|
|
||||||
return "".join([word.text for word in self.words])
|
|
||||||
|
|
||||||
@redis_cache(prefix="profanity", duration=3600 * 24 * 7)
|
|
||||||
def _get_censored_text(self, text: str):
|
|
||||||
return profanity_filter.censor(text).strip()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text(self):
|
def text(self):
|
||||||
# Censored text
|
return "".join([word.text for word in self.words])
|
||||||
return self._get_censored_text(self.raw_text)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def human_timestamp(self):
|
def human_timestamp(self):
|
||||||
@@ -170,12 +154,6 @@ class Transcript(BaseModel):
|
|||||||
word.start += offset
|
word.start += offset
|
||||||
word.end += offset
|
word.end += offset
|
||||||
|
|
||||||
def clone(self):
|
|
||||||
words = [
|
|
||||||
Word(text=word.text, start=word.start, end=word.end) for word in self.words
|
|
||||||
]
|
|
||||||
return Transcript(text=self.text, translation=self.translation, words=words)
|
|
||||||
|
|
||||||
def as_segments(self) -> list[TranscriptSegment]:
|
def as_segments(self) -> list[TranscriptSegment]:
|
||||||
return words_to_segments(self.words)
|
return words_to_segments(self.words)
|
||||||
|
|
||||||
|
|||||||
23
server/uv.lock
generated
23
server/uv.lock
generated
@@ -1340,15 +1340,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" },
|
{ url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "inflection"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091, upload-time = "2020-08-22T08:16:29.139Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
@@ -2313,18 +2304,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/74/c1/bb7e334135859c3a92ec399bc89293ea73f28e815e35b43929c8db6af030/primePy-1.3-py3-none-any.whl", hash = "sha256:5ed443718765be9bf7e2ff4c56cdff71b42140a15b39d054f9d99f0009e2317a", size = 4040, upload-time = "2018-05-29T17:18:17.53Z" },
|
{ url = "https://files.pythonhosted.org/packages/74/c1/bb7e334135859c3a92ec399bc89293ea73f28e815e35b43929c8db6af030/primePy-1.3-py3-none-any.whl", hash = "sha256:5ed443718765be9bf7e2ff4c56cdff71b42140a15b39d054f9d99f0009e2317a", size = 4040, upload-time = "2018-05-29T17:18:17.53Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "profanityfilter"
|
|
||||||
version = "2.1.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "inflection" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8d/03/08740b5e0800f9eb9f675c149a497a3f3735e7b04e414bcce64136e7e487/profanityfilter-2.1.0.tar.gz", hash = "sha256:0ede04e92a9d7255faa52b53776518edc6586dda828aca677c74b5994dfdd9d8", size = 7910, upload-time = "2024-11-25T22:31:51.194Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/03/eb18f72dc6e6398e75e3762677f18ab3a773a384b18efd3ed9119844e892/profanityfilter-2.1.0-py2.py3-none-any.whl", hash = "sha256:e1bc07012760fd74512a335abb93a36877831ed26abab78bfe31bebb68f8c844", size = 7483, upload-time = "2024-11-25T22:31:50.129Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prometheus-client"
|
name = "prometheus-client"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@@ -3133,7 +3112,6 @@ dependencies = [
|
|||||||
{ name = "llama-index-llms-openai-like" },
|
{ name = "llama-index-llms-openai-like" },
|
||||||
{ name = "nltk" },
|
{ name = "nltk" },
|
||||||
{ name = "openai" },
|
{ name = "openai" },
|
||||||
{ name = "profanityfilter" },
|
|
||||||
{ name = "prometheus-fastapi-instrumentator" },
|
{ name = "prometheus-fastapi-instrumentator" },
|
||||||
{ name = "protobuf" },
|
{ name = "protobuf" },
|
||||||
{ name = "psycopg2-binary" },
|
{ name = "psycopg2-binary" },
|
||||||
@@ -3210,7 +3188,6 @@ requires-dist = [
|
|||||||
{ name = "llama-index-llms-openai-like", specifier = ">=0.4.0" },
|
{ name = "llama-index-llms-openai-like", specifier = ">=0.4.0" },
|
||||||
{ name = "nltk", specifier = ">=3.8.1" },
|
{ name = "nltk", specifier = ">=3.8.1" },
|
||||||
{ name = "openai", specifier = ">=1.59.7" },
|
{ name = "openai", specifier = ">=1.59.7" },
|
||||||
{ name = "profanityfilter", specifier = ">=2.0.6" },
|
|
||||||
{ name = "prometheus-fastapi-instrumentator", specifier = ">=6.1.0" },
|
{ name = "prometheus-fastapi-instrumentator", specifier = ">=6.1.0" },
|
||||||
{ name = "protobuf", specifier = ">=4.24.3" },
|
{ name = "protobuf", specifier = ">=4.24.3" },
|
||||||
{ name = "psycopg2-binary", specifier = ">=2.9.10" },
|
{ name = "psycopg2-binary", specifier = ">=2.9.10" },
|
||||||
|
|||||||
@@ -33,17 +33,27 @@ export default function Player(props: PlayerProps) {
|
|||||||
const topicsRef = useRef(props.topics);
|
const topicsRef = useRef(props.topics);
|
||||||
const [firstRender, setFirstRender] = useState<boolean>(true);
|
const [firstRender, setFirstRender] = useState<boolean>(true);
|
||||||
|
|
||||||
const keyHandler = (e) => {
|
const shouldIgnoreHotkeys = (target: EventTarget | null) => {
|
||||||
if (e.key == " ") {
|
return (
|
||||||
|
target instanceof HTMLInputElement ||
|
||||||
|
target instanceof HTMLTextAreaElement
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyHandler = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === " ") {
|
||||||
|
if (e.repeat) return;
|
||||||
|
if (shouldIgnoreHotkeys(e.target)) return;
|
||||||
|
e.preventDefault();
|
||||||
wavesurfer?.playPause();
|
wavesurfer?.playPause();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener("keyup", keyHandler);
|
document.addEventListener("keydown", keyHandler);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keyup", keyHandler);
|
document.removeEventListener("keydown", keyHandler);
|
||||||
};
|
};
|
||||||
});
|
}, [wavesurfer]);
|
||||||
|
|
||||||
// Waveform setup
|
// Waveform setup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
Combobox,
|
Combobox,
|
||||||
Spinner,
|
Spinner,
|
||||||
useFilter,
|
createListCollection,
|
||||||
useListCollection,
|
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { TbBrandZulip } from "react-icons/tb";
|
import { TbBrandZulip } from "react-icons/tb";
|
||||||
import {
|
import {
|
||||||
@@ -48,8 +47,6 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) {
|
|||||||
const { data: topics = [] } = useZulipTopics(selectedStreamId);
|
const { data: topics = [] } = useZulipTopics(selectedStreamId);
|
||||||
const postToZulipMutation = useTranscriptPostToZulip();
|
const postToZulipMutation = useTranscriptPostToZulip();
|
||||||
|
|
||||||
const { contains } = useFilter({ sensitivity: "base" });
|
|
||||||
|
|
||||||
const streamItems = useMemo(() => {
|
const streamItems = useMemo(() => {
|
||||||
return streams.map((stream: Stream) => ({
|
return streams.map((stream: Stream) => ({
|
||||||
label: stream.name,
|
label: stream.name,
|
||||||
@@ -64,17 +61,21 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) {
|
|||||||
}));
|
}));
|
||||||
}, [topics]);
|
}, [topics]);
|
||||||
|
|
||||||
const { collection: streamItemsCollection, filter: streamItemsFilter } =
|
const streamCollection = useMemo(
|
||||||
useListCollection({
|
() =>
|
||||||
initialItems: streamItems,
|
createListCollection({
|
||||||
filter: contains,
|
items: streamItems,
|
||||||
});
|
}),
|
||||||
|
[streamItems],
|
||||||
|
);
|
||||||
|
|
||||||
const { collection: topicItemsCollection, filter: topicItemsFilter } =
|
const topicCollection = useMemo(
|
||||||
useListCollection({
|
() =>
|
||||||
initialItems: topicItems,
|
createListCollection({
|
||||||
filter: contains,
|
items: topicItems,
|
||||||
});
|
}),
|
||||||
|
[topicItems],
|
||||||
|
);
|
||||||
|
|
||||||
// Update selected stream ID when stream changes
|
// Update selected stream ID when stream changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -156,15 +157,12 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) {
|
|||||||
<Flex align="center" gap={2}>
|
<Flex align="center" gap={2}>
|
||||||
<Text>#</Text>
|
<Text>#</Text>
|
||||||
<Combobox.Root
|
<Combobox.Root
|
||||||
collection={streamItemsCollection}
|
collection={streamCollection}
|
||||||
value={stream ? [stream] : []}
|
value={stream ? [stream] : []}
|
||||||
onValueChange={(e) => {
|
onValueChange={(e) => {
|
||||||
setTopic(undefined);
|
setTopic(undefined);
|
||||||
setStream(e.value[0]);
|
setStream(e.value[0]);
|
||||||
}}
|
}}
|
||||||
onInputValueChange={(e) =>
|
|
||||||
streamItemsFilter(e.inputValue)
|
|
||||||
}
|
|
||||||
openOnClick={true}
|
openOnClick={true}
|
||||||
positioning={{
|
positioning={{
|
||||||
strategy: "fixed",
|
strategy: "fixed",
|
||||||
@@ -181,7 +179,7 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) {
|
|||||||
<Combobox.Positioner>
|
<Combobox.Positioner>
|
||||||
<Combobox.Content>
|
<Combobox.Content>
|
||||||
<Combobox.Empty>No streams found</Combobox.Empty>
|
<Combobox.Empty>No streams found</Combobox.Empty>
|
||||||
{streamItemsCollection.items.map((item) => (
|
{streamItems.map((item) => (
|
||||||
<Combobox.Item key={item.value} item={item}>
|
<Combobox.Item key={item.value} item={item}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Combobox.Item>
|
</Combobox.Item>
|
||||||
@@ -197,12 +195,9 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) {
|
|||||||
<Flex align="center" gap={2}>
|
<Flex align="center" gap={2}>
|
||||||
<Text visibility="hidden">#</Text>
|
<Text visibility="hidden">#</Text>
|
||||||
<Combobox.Root
|
<Combobox.Root
|
||||||
collection={topicItemsCollection}
|
collection={topicCollection}
|
||||||
value={topic ? [topic] : []}
|
value={topic ? [topic] : []}
|
||||||
onValueChange={(e) => setTopic(e.value[0])}
|
onValueChange={(e) => setTopic(e.value[0])}
|
||||||
onInputValueChange={(e) =>
|
|
||||||
topicItemsFilter(e.inputValue)
|
|
||||||
}
|
|
||||||
openOnClick
|
openOnClick
|
||||||
selectionBehavior="replace"
|
selectionBehavior="replace"
|
||||||
skipAnimationOnMount={true}
|
skipAnimationOnMount={true}
|
||||||
@@ -222,7 +217,7 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) {
|
|||||||
<Combobox.Positioner>
|
<Combobox.Positioner>
|
||||||
<Combobox.Content>
|
<Combobox.Content>
|
||||||
<Combobox.Empty>No topics found</Combobox.Empty>
|
<Combobox.Empty>No topics found</Combobox.Empty>
|
||||||
{topicItemsCollection.items.map((item) => (
|
{topicItems.map((item) => (
|
||||||
<Combobox.Item key={item.value} item={item}>
|
<Combobox.Item key={item.value} item={item}>
|
||||||
{item.label}
|
{item.label}
|
||||||
<Combobox.ItemIndicator />
|
<Combobox.ItemIndicator />
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
import createClient from "openapi-fetch";
|
import createClient from "openapi-fetch";
|
||||||
import type { paths } from "../reflector-api";
|
import type { paths } from "../reflector-api";
|
||||||
import createFetchClient from "openapi-react-query";
|
import createFetchClient from "openapi-react-query";
|
||||||
import { assertExistsAndNonEmptyString } from "./utils";
|
import { assertExistsAndNonEmptyString, parseNonEmptyString } from "./utils";
|
||||||
import { isBuildPhase } from "./next";
|
import { isBuildPhase } from "./next";
|
||||||
|
import { getSession } from "next-auth/react";
|
||||||
|
import { assertExtendedToken } from "./types";
|
||||||
|
|
||||||
export const API_URL = !isBuildPhase
|
export const API_URL = !isBuildPhase
|
||||||
? assertExistsAndNonEmptyString(
|
? assertExistsAndNonEmptyString(
|
||||||
@@ -21,29 +23,29 @@ export const client = createClient<paths>({
|
|||||||
baseUrl: API_URL,
|
baseUrl: API_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
const waitForAuthTokenDefinitivePresenceOrAbscence = async () => {
|
// will assert presence/absence of login initially
|
||||||
let tries = 0;
|
const initialSessionPromise = getSession();
|
||||||
let time = 0;
|
|
||||||
const STEP = 100;
|
const waitForAuthTokenDefinitivePresenceOrAbsence = async () => {
|
||||||
while (currentAuthToken === undefined) {
|
const initialSession = await initialSessionPromise;
|
||||||
await new Promise((resolve) => setTimeout(resolve, STEP));
|
if (currentAuthToken === undefined) {
|
||||||
time += STEP;
|
currentAuthToken =
|
||||||
tries++;
|
initialSession === null
|
||||||
// most likely first try is more than enough, if it's more there's already something weird happens
|
? null
|
||||||
if (tries > 10) {
|
: assertExtendedToken(initialSession).accessToken;
|
||||||
// even when there's no auth assumed at all, we probably should explicitly call configureApiAuth(null)
|
|
||||||
throw new Error(
|
|
||||||
`Could not get auth token definitive presence/absence in ${time}ms. not calling configureApiAuth?`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// otherwise already overwritten by external forces
|
||||||
|
return currentAuthToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
client.use({
|
client.use({
|
||||||
async onRequest({ request }) {
|
async onRequest({ request }) {
|
||||||
await waitForAuthTokenDefinitivePresenceOrAbscence();
|
const token = await waitForAuthTokenDefinitivePresenceOrAbsence();
|
||||||
if (currentAuthToken) {
|
if (token !== null) {
|
||||||
request.headers.set("Authorization", `Bearer ${currentAuthToken}`);
|
request.headers.set(
|
||||||
|
"Authorization",
|
||||||
|
`Bearer ${parseNonEmptyString(token)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// XXX Only set Content-Type if not already set (FormData will set its own boundary)
|
// XXX Only set Content-Type if not already set (FormData will set its own boundary)
|
||||||
// This is a work around for uploading file, we're passing a formdata
|
// This is a work around for uploading file, we're passing a formdata
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export interface CustomSession extends Session {
|
|||||||
// assumption that JWT is JWTWithAccessToken - we set it in jwt callback of auth; typing isn't strong around there
|
// assumption that JWT is JWTWithAccessToken - we set it in jwt callback of auth; typing isn't strong around there
|
||||||
// but the assumption is crucial to auth working
|
// but the assumption is crucial to auth working
|
||||||
export const assertExtendedToken = <T>(
|
export const assertExtendedToken = <T>(
|
||||||
t: T,
|
t: Exclude<T, null | undefined>,
|
||||||
): T & {
|
): T & {
|
||||||
accessTokenExpires: number;
|
accessTokenExpires: number;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
@@ -45,7 +45,7 @@ export const assertExtendedToken = <T>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const assertExtendedTokenAndUserId = <U, T extends { user?: U }>(
|
export const assertExtendedTokenAndUserId = <U, T extends { user?: U }>(
|
||||||
t: T,
|
t: Exclude<T, null | undefined>,
|
||||||
): T & {
|
): T & {
|
||||||
accessTokenExpires: number;
|
accessTokenExpires: number;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
@@ -55,7 +55,7 @@ export const assertExtendedTokenAndUserId = <U, T extends { user?: U }>(
|
|||||||
} => {
|
} => {
|
||||||
const extendedToken = assertExtendedToken(t);
|
const extendedToken = assertExtendedToken(t);
|
||||||
if (typeof (extendedToken.user as any)?.id === "string") {
|
if (typeof (extendedToken.user as any)?.id === "string") {
|
||||||
return t as T & {
|
return t as Exclude<T, null | undefined> & {
|
||||||
accessTokenExpires: number;
|
accessTokenExpires: number;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
user: U & {
|
user: U & {
|
||||||
@@ -67,7 +67,9 @@ export const assertExtendedTokenAndUserId = <U, T extends { user?: U }>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// best attempt to check the session is valid
|
// best attempt to check the session is valid
|
||||||
export const assertCustomSession = <S extends Session>(s: S): CustomSession => {
|
export const assertCustomSession = <T extends Session>(
|
||||||
|
s: Exclude<T, null | undefined>,
|
||||||
|
): CustomSession => {
|
||||||
const r = assertExtendedTokenAndUserId(s);
|
const r = assertExtendedTokenAndUserId(s);
|
||||||
// no other checks for now
|
// no other checks for now
|
||||||
return r as CustomSession;
|
return r as CustomSession;
|
||||||
|
|||||||
Reference in New Issue
Block a user