+
@@ -27,16 +28,10 @@ const accountStore = useAccountStore();
:me="accountStore.account.nick"
/>
-
-
-
-
-
-
-
-
-
-
+ ircStore.client.raw(txt)"
+ @send="ircStore.sendActiveBuffer"
+ />
diff --git a/app/frontend/src/components/InputBuffer.vue b/app/frontend/src/components/InputBuffer.vue
index 4c91440..7084da9 100644
--- a/app/frontend/src/components/InputBuffer.vue
+++ b/app/frontend/src/components/InputBuffer.vue
@@ -1,10 +1,16 @@
-
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+
+
diff --git a/app/frontend/src/plugins/index.ts b/app/frontend/src/plugins/index.ts
index 4d72803..e71430b 100644
--- a/app/frontend/src/plugins/index.ts
+++ b/app/frontend/src/plugins/index.ts
@@ -1,8 +1,9 @@
import vuetify from "./vuetify";
import pinia from "@/stores";
+import router from "./router.ts";
import type { App } from "vue";
export function registerPlugins(app: App) {
- app.use(vuetify).use(pinia);
+ app.use(vuetify).use(router).use(pinia);
}
diff --git a/app/frontend/src/plugins/router.ts b/app/frontend/src/plugins/router.ts
new file mode 100644
index 0000000..a4bf648
--- /dev/null
+++ b/app/frontend/src/plugins/router.ts
@@ -0,0 +1,37 @@
+import { createMemoryHistory, createRouter } from "vue-router";
+import Chat from "@/components/Chat.vue";
+import { useIRCStore } from "@/stores/irc";
+const routes = [
+ {
+ path: "/",
+ name: "Chat",
+ component: Chat,
+ children: [
+ {
+ path: "register",
+ name: "Register",
+ component: () => import("@/components/Register.vue"),
+ },
+ ],
+ },
+ {
+ path: "/login",
+ name: "Login",
+ component: () => import("@/components/Login.vue"),
+ },
+];
+
+const router = createRouter({
+ history: createMemoryHistory(),
+ routes,
+});
+
+router.beforeEach(async (to, from) => {
+ if (!useIRCStore().connected && to.name !== "Login") {
+ return { name: "Login" };
+ }
+
+ return true;
+});
+
+export default router;
diff --git a/app/frontend/src/stores/accountStore.ts b/app/frontend/src/stores/accountStore.ts
index dafa019..b98d725 100644
--- a/app/frontend/src/stores/accountStore.ts
+++ b/app/frontend/src/stores/accountStore.ts
@@ -3,6 +3,7 @@ import { ref } from "vue";
export const useAccountStore = defineStore("accountStore", () => {
const authenticated = ref(false);
+ const showRegistration = ref(false);
const account = ref({
nick: "",
account: "",
@@ -21,5 +22,13 @@ export const useAccountStore = defineStore("accountStore", () => {
account.value.nick = v;
}
- return { account, authError, authenticated, setAuthenticated, setNick };
+ return {
+ account,
+ authError,
+ authenticated,
+ showRegistration,
+ setAuthenticated,
+ setNick,
+ showRegistration,
+ };
});
diff --git a/app/frontend/src/stores/irc.ts b/app/frontend/src/stores/irc.ts
index 7183d0f..5b7d59c 100644
--- a/app/frontend/src/stores/irc.ts
+++ b/app/frontend/src/stores/irc.ts
@@ -1,15 +1,50 @@
import { defineStore, storeToRefs } from "pinia";
+import { useRouter } from "vue-router";
import { Client } from "irc-framework";
import { useBufferStore } from "./bufferStore";
import { ref } from "vue";
import { useAccountStore } from "./accountStore";
+export type HookFunction = (event: any) => HookStatus;
+
+export enum HookStatus {
+ HOOK_OK,
+ HOOK_EAT,
+}
+
export const useIRCStore = defineStore("ircStore", () => {
const bufferStore = useBufferStore();
const accountStore = useAccountStore();
const connected = ref(false);
const { authError } = storeToRefs(accountStore);
+ const hooks = {} as Record;
+
+ function registerHook(event: string, f: HookFunction) {
+ if (hooks[event]) hooks[event].push(f);
+ else hooks[event] = [f];
+ }
+
+ function runHook(eventName: string, eventArgs: any): HookStatus {
+ if (!hooks[eventName]) return HookStatus.HOOK_OK;
+ let lastRetVal = HookStatus.HOOK_OK;
+
+ for (const hookFunction of hooks[eventName]) {
+ const retVal = hookFunction(eventArgs);
+ if (retVal === HookStatus.HOOK_EAT) return retVal;
+ lastRetVal = retVal;
+ }
+
+ return lastRetVal;
+ }
+
+ function unregisterHook(eventName: string, f: HookFunction) {
+ if (!hooks[eventName]) return;
+ const idx = hooks[eventName].findIndex((item) => item === f);
+ if (idx === -1) return;
+ hooks[eventName].splice(idx, 1);
+ }
+
const selfAvatar = ref("https://placekittens.com/128/128");
const bio = ref();
@@ -85,7 +120,6 @@ export const useIRCStore = defineStore("ircStore", () => {
nick: accountStore.account.nick,
};
- console.log(connectParams);
client.connect(connectParams);
}
@@ -93,11 +127,12 @@ export const useIRCStore = defineStore("ircStore", () => {
if (!bufferStore.activeBuffer) {
return;
}
- bufferStore.activeBuffer.channel.say(message);
+ if (bufferStore.activeBuffer.channel)
+ bufferStore.activeBuffer.channel.say(message);
+ else client.say(bufferStore.activeBuffer.name, message);
}
function isMe(target: string) {
- console.log(client.user.nick);
return target === client.user.nick;
}
@@ -129,8 +164,10 @@ export const useIRCStore = defineStore("ircStore", () => {
},
);
+ const router = useRouter();
client.on("registered", function () {
connected.value = true;
+ router.push({ name: "Chat" });
client.list();
client.raw("METADATA * SUB avatar");
client.raw("METADATA * SUB bio");
@@ -207,14 +244,22 @@ export const useIRCStore = defineStore("ircStore", () => {
client.on("message", function (message: { nick: string; target: string }) {
let buffer;
+
+ const retVal = runHook("message", message);
+ if (retVal === HookStatus.HOOK_EAT) return;
+
if (message.nick === "HistServ") return;
if (isMe(message.target)) {
buffer = bufferStore.getBuffer(message.nick);
} else {
buffer = bufferStore.getBuffer(message.target);
}
+
if (!buffer) {
- return;
+ buffer = bufferStore.addBuffer(message.nick, {
+ name: message.nick,
+ channel: null,
+ });
}
buffer.messages.push(message);
@@ -227,6 +272,14 @@ export const useIRCStore = defineStore("ircStore", () => {
}
});
+ client.on("notice", function (message) {
+ const retVal = runHook("notice", message);
+ if (retVal === HookStatus.HOOK_EAT) return;
+ if (bufferStore.activeBuffer) {
+ bufferStore.activeBuffer.messages.push({ ...message, kind: "notice" });
+ }
+ });
+
client.on("join", ({ nick, channel }: { nick: string; channel: string }) => {
if (isMe(nick)) {
bufferStore.addBuffer(channel, {
@@ -294,5 +347,7 @@ export const useIRCStore = defineStore("ircStore", () => {
setBio,
bio,
connected,
+ registerHook,
+ unregisterHook,
};
});
diff --git a/app/package-lock.json b/app/package-lock.json
index 7537f17..dcf9180 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -1,6 +1,301 @@
{
- "name": "IrChad",
+ "name": "app",
"lockfileVersion": 3,
"requires": true,
- "packages": {}
+ "packages": {
+ "": {
+ "dependencies": {
+ "vue-router": "^4.6.4"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
+ "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.6"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
+ "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.26",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz",
+ "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@vue/shared": "3.5.26",
+ "entities": "^7.0.0",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.26",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz",
+ "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.26",
+ "@vue/shared": "3.5.26"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.26",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz",
+ "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@vue/compiler-core": "3.5.26",
+ "@vue/compiler-dom": "3.5.26",
+ "@vue/compiler-ssr": "3.5.26",
+ "@vue/shared": "3.5.26",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.6",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.26",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz",
+ "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.26",
+ "@vue/shared": "3.5.26"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.26",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz",
+ "integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.26"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.26",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.26.tgz",
+ "integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.26",
+ "@vue/shared": "3.5.26"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.26",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz",
+ "integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.26",
+ "@vue/runtime-core": "3.5.26",
+ "@vue/shared": "3.5.26",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.26",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz",
+ "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.26",
+ "@vue/shared": "3.5.26"
+ },
+ "peerDependencies": {
+ "vue": "3.5.26"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.26",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz",
+ "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==",
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/entities": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz",
+ "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.5.26",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
+ "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.26",
+ "@vue/compiler-sfc": "3.5.26",
+ "@vue/runtime-dom": "3.5.26",
+ "@vue/server-renderer": "3.5.26",
+ "@vue/shared": "3.5.26"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-router": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
+ "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ }
+ }
}
diff --git a/app/package.json b/app/package.json
new file mode 100644
index 0000000..06fd376
--- /dev/null
+++ b/app/package.json
@@ -0,0 +1,5 @@
+{
+ "dependencies": {
+ "vue-router": "^4.6.4"
+ }
+}