Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
./src-tauri/target/*
16 changes: 8 additions & 8 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@ services:
dockerfile: Dockerfile
container_name: obsidian-irc
ports:
- "3000:80"
- '3000:80'
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/"]
test: ['CMD', 'wget', '-qO-', 'http://localhost/']
interval: 30s
timeout: 10s
retries: 3

# TODO - Add IRC daemon + backend etc
# TODO - Add IRC daemon + backend etc
ircd:
init: true
# TODO: Use our unrealircd custom image instead
image: ghcr.io/ergochat/ergo
container_name: ergo
ports:
- "8097:8097"
- "6667:6667"
- '8097:8097'
- '6667:6667'
restart: unless-stopped
volumes:
- ./docker/ergo.yaml:/ircd/ircd.yaml
Expand All @@ -41,7 +41,7 @@ services:
PORT: 6667
CHANNEL: '#test'
NICK: EchoBot
command: ["python", "echobot.py"]
command: ['python', '-u', 'echobot.py']
restart: unless-stopped
profiles:
- testing
Expand All @@ -59,7 +59,7 @@ services:
PORT: 6667
CHANNEL: '#test'
NICK: HelperBot1
command: ["python", "echobot.py"]
command: ['python', '-u', 'echobot.py']
restart: unless-stopped
profiles:
- testing
Expand All @@ -77,7 +77,7 @@ services:
PORT: 6667
CHANNEL: '#test'
NICK: HelperBot2
command: ["python", "echobot.py"]
command: ['python', '-u', 'echobot.py']
restart: unless-stopped
profiles:
- testing
23 changes: 14 additions & 9 deletions docker/echobot.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import re
import socket
import time
import re


def main():
Expand Down Expand Up @@ -38,18 +38,23 @@ def send(msg: str):
sender = prefix.split("!")[0][1:]
message = " ".join(msg_parts)[1:]

# Only respond to messages sent by other users to the joined channel
if sender != NICK and target == CHANNEL:
if sender != NICK:
# Check if bot is mentioned using word boundary or at start with colon
nick_mentioned = (
re.search(rf'\b{re.escape(NICK)}\b', message, re.IGNORECASE) or
re.match(rf'^{re.escape(NICK)}:\s+', message, re.IGNORECASE)
nick_mentioned = re.search(rf"\b{re.escape(NICK)}\b", message, re.IGNORECASE) or re.match(
rf"^{re.escape(NICK)}:\s+", message, re.IGNORECASE
)
print(f"Message from {sender} in {target}: {message}")

if target == NICK:
# Private message
send(f"@+typing=active TAGMSG {sender}")
time.sleep(2)
send(f"PRIVMSG {sender} :You said: {message}")

Comment on lines +48 to 53
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

❓ Verification inconclusive

TAGMSG requires CAP negotiation; gate typing tags and make delay configurable

Unconditional TAGMSG can fail (421/unknown command) on servers without message-tags/typing; 2s sleep blocks PING handling under load.

-                    if target == NICK:
-                        # Private message
-                        send(f"@+typing=active TAGMSG {sender}")
-                        time.sleep(2)
-                        send(f"PRIVMSG {sender} :You said: {message}")
+                    if target == NICK:
+                        # Private message
+                        if TYPING_TAGS:
+                            send(f"@+typing=active TAGMSG {sender}")
+                        time.sleep(TYPING_DELAY)
+                        send(f"PRIVMSG {sender} :You said: {message}")

Add these near the top (outside this hunk) to configure behavior:

# config
TYPING_TAGS = os.environ.get("TYPING_TAGS", "0") == "1"
TYPING_DELAY = float(os.environ.get("TYPING_DELAY", "0.3"))

Optional (only if TYPING_TAGS is enabled): request capabilities after connect and before JOIN:

if TYPING_TAGS:
    send("CAP LS 302")
    send("CAP REQ :message-tags draft/typing")
    send("CAP END")

Please verify the target IRCd supports message-tags/typing when TYPING_TAGS=1.


Gate TAGMSG behind CAP and avoid blocking sleep; make typing delay configurable

File: docker/echobot.py Lines: 48-53

  • TAGMSG requires message-tags/typing CAP — do not send unconditionally; gate behind a config flag and request capabilities when enabled.
  • time.sleep(2) is blocking and can prevent PING handling under load — replace with a non-blocking timer or a configurable short delay.
-                    if target == NICK:
-                        # Private message
-                        send(f"@+typing=active TAGMSG {sender}")
-                        time.sleep(2)
-                        send(f"PRIVMSG {sender} :You said: {message}")
+                    if target == NICK:
+                        # Private message
+                        if TYPING_TAGS:
+                            send(f"@+typing=active TAGMSG {sender}")
+                        time.sleep(TYPING_DELAY)
+                        send(f"PRIVMSG {sender} :You said: {message}")

Add near the top (outside this hunk):

# config
TYPING_TAGS = os.environ.get("TYPING_TAGS", "0") == "1"
TYPING_DELAY = float(os.environ.get("TYPING_DELAY", "0.3"))

Optional (if TYPING_TAGS enabled): request capabilities after connect and before JOIN:

if TYPING_TAGS:
    send("CAP LS 302")
    send("CAP REQ :message-tags draft/typing")
    send("CAP END")

Verify the target IRCd supports message-tags/typing when TYPING_TAGS=1.

🤖 Prompt for AI Agents
docker/echobot.py lines 48-53: TAGMSG is being sent unconditionally and
time.sleep(2) blocks the main loop; add two config flags at module top
(TYPING_TAGS from env "0"/"1" and TYPING_DELAY as float from env, default ~0.3),
request the message-tags/draft/typing capabilities after connecting and before
JOIN only if TYPING_TAGS is true, gate sending the TAGMSG behind TYPING_TAGS so
you don't send tags when unsupported, and replace the blocking time.sleep with a
non-blocking mechanism (e.g., schedule the delayed PRIVMSG with threading.Timer
or an equivalent async timer so PING handling isn't blocked) while using the
configured TYPING_DELAY; also document that TYPING_TAGS must only be enabled if
the IRCd supports message-tags/typing.

if nick_mentioned:
send(f"@+typing=active TAGMSG {CHANNEL}")
elif nick_mentioned:
send(f"@+typing=active TAGMSG {target}")
time.sleep(2) # Simulate typing delay
send(f"PRIVMSG {CHANNEL} :{sender}: I heard you mention me! You said: {message}")
send(f"PRIVMSG {target} :{sender}: I heard you mention me! You said: {message}")


if __name__ == "__main__":
Expand Down
6 changes: 5 additions & 1 deletion src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ export const AppLayout: React.FC = () => {
isChannelListVisible,
mobileViewActiveColumn,
selectedServerId,
selectedPrivateChatId,
},
toggleMobileMenu,
toggleMemberList,
toggleChannelList,
setMobileViewActiveColumn,
} = useStore();

// Hide member list for private chats
const shouldShowMemberList = isMemberListVisible && !selectedPrivateChatId;

// Set theme class on body
useEffect(() => {
document.body.classList.toggle("dark", isDarkMode);
Expand Down Expand Up @@ -107,7 +111,7 @@ export const AppLayout: React.FC = () => {
return (
<ResizableSidebar
bypass={isNarrowView && mobileViewActiveColumn === "memberList"}
isVisible={isMemberListVisible}
isVisible={shouldShowMemberList}
defaultWidth={240}
minWidth={80}
maxWidth={400}
Expand Down
99 changes: 98 additions & 1 deletion src/components/layout/ChannelList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,36 @@ import {
FaHashtag,
FaPlus,
FaTrash,
FaUser,
FaUserPlus,
FaVolumeUp,
} from "react-icons/fa";
import { useMediaQuery } from "../../hooks/useMediaQuery";
import useStore from "../../store";
import TouchableContextMenu from "../mobile/TouchableContextMenu";
import AddPrivateChatModal from "../ui/AddPrivateChatModal";

export const ChannelList: React.FC<{
onToggle: () => void;
}> = ({ onToggle }: { onToggle: () => void }) => {
const {
servers,
ui: { selectedServerId, selectedChannelId },
ui: { selectedServerId, selectedChannelId, selectedPrivateChatId },
selectChannel,
selectPrivateChat,
joinChannel,
leaveChannel,
deletePrivateChat,
toggleUserProfileModal,
currentUser,
} = useStore();

const [isTextChannelsOpen, setIsTextChannelsOpen] = useState(true);
const [isVoiceChannelsOpen, setIsVoiceChannelsOpen] = useState(true);
const [isPrivateChatsOpen, setIsPrivateChatsOpen] = useState(true);
const [newChannelName, setNewChannelName] = useState("");
const [isAddPrivateChatModalOpen, setIsAddPrivateChatModalOpen] =
useState(false);

const selectedServer = servers.find(
(server) => server.id === selectedServerId,
Expand Down Expand Up @@ -264,6 +271,87 @@ export const ChannelList: React.FC<{
</div>
)}
</div>

{/* Private Messages */}
<div className="mb-2">
<div
className="flex items-center px-2 group cursor-pointer mb-1"
onClick={() => setIsPrivateChatsOpen(!isPrivateChatsOpen)}
>
{isPrivateChatsOpen ? (
<FaChevronDown className="text-xs mr-1" />
) : (
<FaChevronRight className="text-xs mr-1" />
)}
<span className="uppercase text-xs font-semibold tracking-wide">
Private Messages
</span>
<FaPlus
className={`ml-auto ${!isNarrowView && "opacity-0 group-hover:opacity-100"} cursor-pointer`}
onClick={(e) => {
e.stopPropagation();
setIsAddPrivateChatModalOpen(true);
}}
/>
</div>

{isPrivateChatsOpen && (
<div className="ml-2">
{selectedServer.privateChats?.map((privateChat) => (
<TouchableContextMenu
key={privateChat.id}
menuItems={[
{
label: "Delete Private Chat",
icon: <FaTrash size={14} />,
onClick: () => {
if (selectedServerId) {
deletePrivateChat(
selectedServerId,
privateChat.id,
);
}
},
className: "text-red-400",
},
]}
>
<div
className={`
px-2 py-1 mb-1 rounded flex items-center justify-between group cursor-pointer
${selectedPrivateChatId === privateChat.id ? "bg-discord-dark-400 text-white" : "hover:bg-discord-dark-100 hover:text-discord-channels-active"}
`}
onClick={() => selectPrivateChat(privateChat.id)}
>
<div className="flex items-center gap-2 truncate">
<FaUser className="shrink-0" />
<span className="truncate">
{privateChat.username}
</span>
</div>
{/* Delete Button */}
{selectedPrivateChatId === privateChat.id && (
<button
className="hidden group-hover:block text-discord-red hover:text-white"
onClick={(e) => {
e.stopPropagation();
if (selectedServerId) {
deletePrivateChat(
selectedServerId,
privateChat.id,
);
}
}}
>
<FaTrash />
</button>
)}
</div>
</TouchableContextMenu>
))}
</div>
)}
</div>
</>
)}
</div>
Expand Down Expand Up @@ -316,6 +404,15 @@ export const ChannelList: React.FC<{
</button>
</div>
</div>

{/* Add Private Chat Modal */}
{selectedServerId && (
<AddPrivateChatModal
isOpen={isAddPrivateChatModalOpen}
onClose={() => setIsAddPrivateChatModalOpen(false)}
serverId={selectedServerId}
/>
)}
</div>
);
};
Expand Down
Loading