Repository navigation
feat(voice): WebRTC voice/stream channels with text chat #198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 15 commits
e7d9188
2f10087
5e1d0c2
7d7134b
d9a8e37
3ecee75
ac1570d
5da6d4a
ecb3c04
99d861c
bcbc1d3
480be92
980c57c
456c4b8
d5336a3
4d5d42b
c5bec43
24d57bb
c7be554
7f25024
d6aef73
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,107 +2,17 @@ services: | |||||||||||||||||||||||||
| obsidian: | ||||||||||||||||||||||||||
| build: | ||||||||||||||||||||||||||
| context: . | ||||||||||||||||||||||||||
| dockerfile: Dockerfile | ||||||||||||||||||||||||||
| container_name: obsidian-irc | ||||||||||||||||||||||||||
| ports: | ||||||||||||||||||||||||||
| - '3000:80' | ||||||||||||||||||||||||||
| dockerfile: ./Dockerfile | ||||||||||||||||||||||||||
| args: | ||||||||||||||||||||||||||
| VITE_DEFAULT_IRC_SERVER: wss://${IRC_FQDN} | ||||||||||||||||||||||||||
| VITE_DEFAULT_IRC_SERVER_NAME: ${NETWORK_NAME} | ||||||||||||||||||||||||||
| image: obsidianirc:local | ||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this imagdoesn't exist publicy @ValwareIRC DONT TOUCH THIS IN THIS PR |
||||||||||||||||||||||||||
| container_name: obsidianirc | ||||||||||||||||||||||||||
| restart: unless-stopped | ||||||||||||||||||||||||||
| healthcheck: | ||||||||||||||||||||||||||
| test: ['CMD', 'wget', '-qO-', 'http://localhost/'] | ||||||||||||||||||||||||||
| interval: 30s | ||||||||||||||||||||||||||
| timeout: 10s | ||||||||||||||||||||||||||
| retries: 3 | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| gen-certs: | ||||||||||||||||||||||||||
| image: alpine:3.20 | ||||||||||||||||||||||||||
| volumes: | ||||||||||||||||||||||||||
| - ${MKCERT_CAROOT:?Run ./scripts/setup-dev-certs.sh first}:/caroot:ro | ||||||||||||||||||||||||||
| - ./certs:/certs | ||||||||||||||||||||||||||
| command: | ||||||||||||||||||||||||||
| - sh | ||||||||||||||||||||||||||
| - -c | ||||||||||||||||||||||||||
| - | | ||||||||||||||||||||||||||
| openssl x509 -noout -in /certs/server.pem 2>/dev/null && exit 0 | ||||||||||||||||||||||||||
| apk add --no-cache openssl -q | ||||||||||||||||||||||||||
| openssl genrsa -out /certs/server-key.pem 2048 | ||||||||||||||||||||||||||
| openssl req -new -key /certs/server-key.pem -out /tmp/csr.pem -subj "/CN=localhost" | ||||||||||||||||||||||||||
| printf '[SAN]\nsubjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1\n' > /tmp/ext.cnf | ||||||||||||||||||||||||||
| openssl x509 -req -in /tmp/csr.pem \ | ||||||||||||||||||||||||||
| -CA /caroot/rootCA.pem -CAkey /caroot/rootCA-key.pem -set_serial 1 \ | ||||||||||||||||||||||||||
| -out /certs/server.pem -days 825 -sha256 -extfile /tmp/ext.cnf -extensions SAN | ||||||||||||||||||||||||||
| profiles: | ||||||||||||||||||||||||||
| - testing | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # TODO - Add IRC daemon + backend etc | ||||||||||||||||||||||||||
| ircd: | ||||||||||||||||||||||||||
| init: true | ||||||||||||||||||||||||||
| # TODO: Use our unrealircd custom image instead | ||||||||||||||||||||||||||
| image: ghcr.io/ergochat/ergo:master | ||||||||||||||||||||||||||
| container_name: ergo | ||||||||||||||||||||||||||
| ports: | ||||||||||||||||||||||||||
| - '6667:6667' # plain IRC (bots) | ||||||||||||||||||||||||||
| - '6697:6697' # IRC over TLS — ircs://localhost:6697 (Tauri native TCP) | ||||||||||||||||||||||||||
| - '8097:8097' # WebSocket over TLS — wss://localhost:8097 (browser / WebView) | ||||||||||||||||||||||||||
| restart: unless-stopped | ||||||||||||||||||||||||||
| depends_on: | ||||||||||||||||||||||||||
| gen-certs: | ||||||||||||||||||||||||||
| condition: service_completed_successfully | ||||||||||||||||||||||||||
| volumes: | ||||||||||||||||||||||||||
| - ./docker/ergo.yaml:/ircd/ircd.yaml | ||||||||||||||||||||||||||
| - ./certs:/ircd/certs:ro | ||||||||||||||||||||||||||
| profiles: | ||||||||||||||||||||||||||
| - testing | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| echo-bot: | ||||||||||||||||||||||||||
| image: python:3.12-alpine | ||||||||||||||||||||||||||
| container_name: echo-bot | ||||||||||||||||||||||||||
| volumes: | ||||||||||||||||||||||||||
| - ./docker/echobot.py:/app/echobot.py:ro | ||||||||||||||||||||||||||
| working_dir: /app | ||||||||||||||||||||||||||
| depends_on: | ||||||||||||||||||||||||||
| - ircd | ||||||||||||||||||||||||||
| environment: | ||||||||||||||||||||||||||
| HOST: ircd | ||||||||||||||||||||||||||
| PORT: 6667 | ||||||||||||||||||||||||||
| CHANNEL: '#test' | ||||||||||||||||||||||||||
| NICK: EchoBot | ||||||||||||||||||||||||||
| command: ['python', '-u', 'echobot.py'] | ||||||||||||||||||||||||||
| restart: unless-stopped | ||||||||||||||||||||||||||
| profiles: | ||||||||||||||||||||||||||
| - testing | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| helper-bot: | ||||||||||||||||||||||||||
| image: python:3.12-alpine | ||||||||||||||||||||||||||
| container_name: helper-bot | ||||||||||||||||||||||||||
| volumes: | ||||||||||||||||||||||||||
| - ./docker/echobot.py:/app/echobot.py:ro | ||||||||||||||||||||||||||
| working_dir: /app | ||||||||||||||||||||||||||
| depends_on: | ||||||||||||||||||||||||||
| - ircd | ||||||||||||||||||||||||||
| environment: | ||||||||||||||||||||||||||
| HOST: ircd | ||||||||||||||||||||||||||
| PORT: 6667 | ||||||||||||||||||||||||||
| CHANNEL: '#test' | ||||||||||||||||||||||||||
| NICK: HelperBot1 | ||||||||||||||||||||||||||
| command: ['python', '-u', 'echobot.py'] | ||||||||||||||||||||||||||
| restart: unless-stopped | ||||||||||||||||||||||||||
| profiles: | ||||||||||||||||||||||||||
| - testing | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| chat-bot: | ||||||||||||||||||||||||||
| image: python:3.12-alpine | ||||||||||||||||||||||||||
| container_name: chat-bot | ||||||||||||||||||||||||||
| volumes: | ||||||||||||||||||||||||||
| - ./docker/echobot.py:/app/echobot.py:ro | ||||||||||||||||||||||||||
| working_dir: /app | ||||||||||||||||||||||||||
| depends_on: | ||||||||||||||||||||||||||
| - ircd | ||||||||||||||||||||||||||
| environment: | ||||||||||||||||||||||||||
| HOST: ircd | ||||||||||||||||||||||||||
| PORT: 6667 | ||||||||||||||||||||||||||
| CHANNEL: '#test' | ||||||||||||||||||||||||||
| NICK: HelperBot2 | ||||||||||||||||||||||||||
| command: ['python', '-u', 'echobot.py'] | ||||||||||||||||||||||||||
| restart: unless-stopped | ||||||||||||||||||||||||||
| profiles: | ||||||||||||||||||||||||||
| - testing | ||||||||||||||||||||||||||
| expose: | ||||||||||||||||||||||||||
| - "80" | ||||||||||||||||||||||||||
| labels: | ||||||||||||||||||||||||||
| - traefik.enable=true | ||||||||||||||||||||||||||
| - traefik.http.routers.obsidian.rule=Host(`${WEB_FQDN}`) | ||||||||||||||||||||||||||
| - traefik.http.routers.obsidian.tls=true | ||||||||||||||||||||||||||
| - traefik.http.services.obsidian.loadbalancer.server.port=80 | ||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing Traefik network configuration. The service has been changed from published ports to Without a shared network, Traefik cannot discover or route to this container, making the service unreachable. 🔧 Proposed fixDefine and attach a network that Traefik uses: labels:
- traefik.enable=true
- traefik.http.routers.obsidian.rule=Host(`${WEB_FQDN}`)
- traefik.http.routers.obsidian.tls=true
- traefik.http.services.obsidian.loadbalancer.server.port=80
+ networks:
+ - traefik-network
+
+networks:
+ traefik-network:
+ external: trueAlternatively, if Traefik is defined in this same compose file, use that network name. 🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incomplete Traefik TLS configuration. The labels enable TLS (
Without these, HTTPS routing may not work as expected. 🔒 Proposed additions labels:
- traefik.enable=true
- traefik.http.routers.obsidian.rule=Host(`${WEB_FQDN}`)
+ - traefik.http.routers.obsidian.entrypoints=websecure
- traefik.http.routers.obsidian.tls=true
+ - traefik.http.routers.obsidian.tls.certresolver=letsencrypt
- traefik.http.services.obsidian.loadbalancer.server.port=80Adjust 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import java.io.File | ||
| import java.util.Properties | ||
|
|
||
| plugins { | ||
|
|
@@ -24,6 +25,28 @@ android { | |
| versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt() | ||
| versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0") | ||
| } | ||
| signingConfigs { | ||
| create("release") { | ||
| val keystorePropsFile = rootProject.file("keystore.properties") | ||
| if (keystorePropsFile.exists()) { | ||
| val keystoreProps = Properties().apply { | ||
| keystorePropsFile.inputStream().use { load(it) } | ||
| } | ||
| storeFile = file(keystoreProps.getProperty("storeFile")) | ||
| storePassword = keystoreProps.getProperty("storePassword") | ||
| keyAlias = keystoreProps.getProperty("keyAlias") | ||
| keyPassword = keystoreProps.getProperty("keyPassword") | ||
| } else { | ||
| val debugKeystore = File(System.getProperty("user.home"), ".android/debug.keystore") | ||
| if (debugKeystore.exists()) { | ||
| storeFile = debugKeystore | ||
| storePassword = "android" | ||
| keyAlias = "androiddebugkey" | ||
| keyPassword = "android" | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+28
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm I think this might break CI builds |
||
| buildTypes { | ||
| getByName("debug") { | ||
| manifestPlaceholders["usesCleartextTraffic"] = "true" | ||
|
|
@@ -38,6 +61,7 @@ android { | |
| } | ||
| getByName("release") { | ||
| isMinifyEnabled = true | ||
| signingConfig = signingConfigs.getByName("release") | ||
| proguardFiles( | ||
| *fileTree(".") { include("**/*.pro") } | ||
| .plus(getDefaultProguardFile("proguard-android-optimize.txt")) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ import { ChatArea } from "./ChatArea"; | |
| import { MemberList } from "./MemberList"; | ||
| import { ResizableSidebar } from "./ResizableSidebar"; | ||
| import { ServerList } from "./ServerList"; | ||
| import { VoiceChannelView } from "./VoiceChannelView"; | ||
|
|
||
| const PAGE_ORDER: layoutColumn[] = ["serverList", "chatView", "memberList"]; | ||
| const getPageIndex = (column: layoutColumn): number => | ||
|
|
@@ -41,7 +42,19 @@ export const AppLayout: React.FC = () => { | |
| selectedChannelId: null, | ||
| selectedPrivateChatId: null, | ||
| }; | ||
| const { selectedPrivateChatId } = currentSelection; | ||
| const { selectedChannelId, selectedPrivateChatId } = currentSelection; | ||
|
|
||
| const selectedChannel = useStore((s) => { | ||
| if (!selectedServerId || !selectedChannelId) return null; | ||
| const srv = s.servers.find((x) => x.id === selectedServerId); | ||
| return srv?.channels.find((c) => c.id === selectedChannelId) ?? null; | ||
| }); | ||
| // Both ^ (voice) and $ (stream) channels render through VoiceChannelView. | ||
| // The view itself reads the room mode from VoiceState.mode and adapts | ||
| // its layout (publishing controls vs viewer chat). | ||
| const isVoiceChannel = | ||
| !!selectedChannel?.name.startsWith("^") || | ||
| !!selectedChannel?.name.startsWith("$"); | ||
| const { | ||
| isDarkMode, | ||
| isMobileMenuOpen, | ||
|
|
@@ -50,8 +63,9 @@ export const AppLayout: React.FC = () => { | |
| mobileViewActiveColumn, | ||
| } = ui; | ||
|
|
||
| // Hide member list for private chats | ||
| const shouldShowMemberList = isMemberListVisible && !selectedPrivateChatId; | ||
| // Hide member list for private chats and voice channels (voice has its own grid) | ||
| const shouldShowMemberList = | ||
| isMemberListVisible && !selectedPrivateChatId && !isVoiceChannel; | ||
|
Comment on lines
+66
to
+68
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hide the mobile member page for voice/stream channels too. This only suppresses the desktop/sidebar member list. On narrow view, the mobile pager still treats the channel as having a member page, so voice channels can still swipe into the old 🤖 Prompt for AI Agents |
||
|
|
||
| const handleChannelListWidthChange = useCallback((width: number) => { | ||
| setChannelListWidth(width); | ||
|
|
@@ -202,16 +216,33 @@ export const AppLayout: React.FC = () => { | |
| <div | ||
| className={`${isNarrowView ? "w-full" : "flex-grow"} h-full bg-discord-dark-200 flex flex-col min-w-0 z-10`} | ||
| > | ||
| <ChatArea | ||
| isChanListVisible={isChannelListVisible} | ||
| onToggleChanList={() => { | ||
| if (isNarrowView) { | ||
| setMobileViewActiveColumn("serverList"); | ||
| } else { | ||
| toggleChannelList(!isChannelListVisible); | ||
| } | ||
| }} | ||
| /> | ||
| {(() => { | ||
| // Both `^` (voice) and `$` (stream) channels render through | ||
| // ChatArea with the VoiceChannelView injected as a topSlot, | ||
| // so the channel's ChatHeader stays at the very top of the | ||
| // panel and the voice grid + chat live beneath it as one | ||
| // continuous column. | ||
| const voiceTopSlot = | ||
| isVoiceChannel && selectedServerId && selectedChannel ? ( | ||
| <VoiceChannelView | ||
| serverId={selectedServerId} | ||
| channelName={selectedChannel.name} | ||
| /> | ||
| ) : undefined; | ||
| return ( | ||
| <ChatArea | ||
| isChanListVisible={isChannelListVisible} | ||
| onToggleChanList={() => { | ||
| if (isNarrowView) { | ||
| setMobileViewActiveColumn("serverList"); | ||
| } else { | ||
| toggleChannelList(!isChannelListVisible); | ||
| } | ||
| }} | ||
| topSlot={voiceTopSlot} | ||
| /> | ||
| ); | ||
| })()} | ||
| </div> | ||
| ); | ||
| case "memberList": | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: ObsidianIRC/ObsidianIRC
Length of output: 132
🏁 Script executed:
Repository: ObsidianIRC/ObsidianIRC
Length of output: 94
🏁 Script executed:
Repository: ObsidianIRC/ObsidianIRC
Length of output: 49
🏁 Script executed:
# Read .env.example to see what's documented cat -n ./.env.exampleRepository: ObsidianIRC/ObsidianIRC
Length of output: 619
🏁 Script executed:
# Search README for environment variable section head -100 README.mdRepository: ObsidianIRC/ObsidianIRC
Length of output: 5066
🏁 Script executed:
Repository: ObsidianIRC/ObsidianIRC
Length of output: 49
🏁 Script executed:
Repository: ObsidianIRC/ObsidianIRC
Length of output: 260
🏁 Script executed:
# Check what's in the docker directory ls -la docker/Repository: ObsidianIRC/ObsidianIRC
Length of output: 368
🏁 Script executed:
Repository: ObsidianIRC/ObsidianIRC
Length of output: 133
Document required environment variables for docker-compose deployment.
The build args in
compose.yaml(lines 6-8) reference${IRC_FQDN},${NETWORK_NAME}, and${WEB_FQDN}(line 16), but these infrastructure-level variables are undocumented. The existing.env.exampledocuments application variables (VITE_*), not the environment variables required for docker-compose deployment. Without documentation, users deploying via docker-compose will not know these required variables must be set.Add a
.envfile or deployment documentation clarifying the required variables:📝 Recommendation
Create a
.envfile at the repository root documenting infrastructure deployment variables:Or add a deployment section to README.md explaining these variables and how to set them before running
docker-compose up.🤖 Prompt for AI Agents