Bring the web client in line with the current API (closes #145)
check / check (push) Waiting to run
check / check (push) Waiting to run
The web client was written against an older shape of the API. MODE now carries its change in `body`, so /mode sets modes; /mode alone queries them and the 324 reply is shown. Numeric replies are matched on `code`, so /list, /who and /whois print their lines, and 353 names are read as hostmasks. The member list takes @ and + from `isOperator` and `isVoiced`. A 401 on the poll reloads the page, which shows the login screen, where it used to retry for ever. The token code is gone: no Authorization header, nothing stored, and the page always asks /state whether it can resume a session. Model: opus-5-5
This commit is contained in:
+95
-58
@@ -58,13 +58,13 @@ async function mintHashcash(bits, resource) {
|
||||
}
|
||||
}
|
||||
|
||||
// Requests are authenticated by the auth cookie, which the browser
|
||||
// sends on its own.
|
||||
function api(path, opts = {}) {
|
||||
const token = localStorage.getItem("neoirc_token");
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
...(opts.headers || {}),
|
||||
};
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
const { signal, ...rest } = opts;
|
||||
|
||||
return fetch(API + path, { ...rest, headers, signal }).then(async (r) => {
|
||||
@@ -122,12 +122,11 @@ function LoginScreen({ onLogin }) {
|
||||
if (s.name) hashcashResourceRef.current = s.name;
|
||||
})
|
||||
.catch(() => {});
|
||||
const saved = localStorage.getItem("neoirc_token");
|
||||
if (saved) {
|
||||
api("/state?initChannelState=1")
|
||||
.then((u) => onLogin(u.nick, true))
|
||||
.catch(() => localStorage.removeItem("neoirc_token"));
|
||||
}
|
||||
// Resume the session if the browser still holds a valid auth
|
||||
// cookie; a 401 leaves the login form up.
|
||||
api("/state?initChannelState=1")
|
||||
.then((u) => onLogin(u.nick, true))
|
||||
.catch(() => {});
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
@@ -152,7 +151,6 @@ function LoginScreen({ onLogin }) {
|
||||
method: "POST",
|
||||
body: JSON.stringify(reqBody),
|
||||
});
|
||||
localStorage.setItem("neoirc_token", res.token);
|
||||
onLogin(res.nick);
|
||||
} catch (err) {
|
||||
setError(err.data?.error || "Connection failed");
|
||||
@@ -227,10 +225,9 @@ function UserList({ members, onNickClick }) {
|
||||
const regular = [];
|
||||
|
||||
for (const m of members) {
|
||||
const mode = m.mode || "";
|
||||
if (mode === "o") {
|
||||
if (m.isOperator) {
|
||||
ops.push(m);
|
||||
} else if (mode === "v") {
|
||||
} else if (m.isVoiced) {
|
||||
voiced.push(m);
|
||||
} else {
|
||||
regular.push(m);
|
||||
@@ -366,7 +363,9 @@ function App() {
|
||||
command: msg.command,
|
||||
};
|
||||
|
||||
switch (msg.command) {
|
||||
// A numeric reply carries its number in `code` and its name in
|
||||
// `command`; other messages have no `code`.
|
||||
switch (msg.code || msg.command) {
|
||||
case "PRIVMSG":
|
||||
case "NOTICE": {
|
||||
let text = body;
|
||||
@@ -464,8 +463,9 @@ function App() {
|
||||
|
||||
break;
|
||||
}
|
||||
case "353": {
|
||||
// RPL_NAMREPLY — parse mode prefixes from names list.
|
||||
case 353: {
|
||||
// RPL_NAMREPLY — each name is a hostmask (nick!user@host),
|
||||
// prefixed with @ for an operator or + for a voiced member.
|
||||
if (
|
||||
Array.isArray(msg.params) &&
|
||||
msg.params.length >= 2 &&
|
||||
@@ -477,21 +477,18 @@ function App() {
|
||||
: String(msg.body);
|
||||
const names = namesStr.split(/\s+/).filter(Boolean);
|
||||
const parsed = names.map((n) => {
|
||||
if (n.startsWith("@")) {
|
||||
return { nick: n.slice(1), mode: "o" };
|
||||
}
|
||||
if (n.startsWith("+")) {
|
||||
return { nick: n.slice(1), mode: "v" };
|
||||
}
|
||||
const isOperator = n.startsWith("@");
|
||||
const isVoiced = n.startsWith("+");
|
||||
const hostmask = isOperator || isVoiced ? n.slice(1) : n;
|
||||
|
||||
return { nick: n, mode: "" };
|
||||
return { nick: hostmask.split("!")[0], isOperator, isVoiced };
|
||||
});
|
||||
setMembers((prev) => ({ ...prev, [channel]: parsed }));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "332": {
|
||||
case 332: {
|
||||
// RPL_TOPIC — set topic from server.
|
||||
if (Array.isArray(msg.params) && msg.params.length >= 1) {
|
||||
const channel = msg.params[0];
|
||||
@@ -503,7 +500,7 @@ function App() {
|
||||
|
||||
break;
|
||||
}
|
||||
case "322": {
|
||||
case 322: {
|
||||
// RPL_LIST — channel, member count, topic.
|
||||
if (Array.isArray(msg.params) && msg.params.length >= 2) {
|
||||
const chName = msg.params[0];
|
||||
@@ -518,7 +515,7 @@ function App() {
|
||||
|
||||
break;
|
||||
}
|
||||
case "323":
|
||||
case 323:
|
||||
addMessage("Server", {
|
||||
...base,
|
||||
text: body || "End of channel list",
|
||||
@@ -526,7 +523,7 @@ function App() {
|
||||
});
|
||||
|
||||
break;
|
||||
case "352": {
|
||||
case 352: {
|
||||
// RPL_WHOREPLY — channel, user, host, server, nick, flags.
|
||||
if (Array.isArray(msg.params) && msg.params.length >= 5) {
|
||||
const whoCh = msg.params[0];
|
||||
@@ -541,7 +538,7 @@ function App() {
|
||||
|
||||
break;
|
||||
}
|
||||
case "315":
|
||||
case 315:
|
||||
addMessage("Server", {
|
||||
...base,
|
||||
text: body || "End of /WHO list",
|
||||
@@ -549,20 +546,20 @@ function App() {
|
||||
});
|
||||
|
||||
break;
|
||||
case "311": {
|
||||
// RPL_WHOISUSER — nick, user, host, *, realname.
|
||||
if (Array.isArray(msg.params) && msg.params.length >= 1) {
|
||||
const wiNick = msg.params[0];
|
||||
case 311: {
|
||||
// RPL_WHOISUSER — params nick, user, host, *; body realname.
|
||||
if (Array.isArray(msg.params) && msg.params.length >= 3) {
|
||||
const [wiNick, wiUser, wiHost] = msg.params;
|
||||
addMessage("Server", {
|
||||
...base,
|
||||
text: `${wiNick} (${body})`,
|
||||
text: `${wiNick} is ${wiUser}@${wiHost} (${body})`,
|
||||
system: true,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "312": {
|
||||
case 312: {
|
||||
// RPL_WHOISSERVER — nick, server, server info.
|
||||
if (Array.isArray(msg.params) && msg.params.length >= 2) {
|
||||
const wiNick = msg.params[0];
|
||||
@@ -576,7 +573,33 @@ function App() {
|
||||
|
||||
break;
|
||||
}
|
||||
case "319": {
|
||||
case 313: {
|
||||
// RPL_WHOISOPERATOR — nick; body "is an IRC operator".
|
||||
if (Array.isArray(msg.params) && msg.params.length >= 1) {
|
||||
addMessage("Server", {
|
||||
...base,
|
||||
text: `${msg.params[0]} ${body}`,
|
||||
system: true,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 317: {
|
||||
// RPL_WHOISIDLE — nick, idle seconds, signon time (Unix).
|
||||
if (Array.isArray(msg.params) && msg.params.length >= 3) {
|
||||
const [wiNick, idle, signon] = msg.params;
|
||||
const signedOn = new Date(Number(signon) * 1000).toLocaleString();
|
||||
addMessage("Server", {
|
||||
...base,
|
||||
text: `${wiNick} has been idle ${idle}s, signed on ${signedOn}`,
|
||||
system: true,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 319: {
|
||||
// RPL_WHOISCHANNELS — nick, channels.
|
||||
if (Array.isArray(msg.params) && msg.params.length >= 1) {
|
||||
const wiNick = msg.params[0];
|
||||
@@ -589,7 +612,7 @@ function App() {
|
||||
|
||||
break;
|
||||
}
|
||||
case "318":
|
||||
case 318:
|
||||
addMessage("Server", {
|
||||
...base,
|
||||
text: body || "End of /WHOIS list",
|
||||
@@ -597,9 +620,21 @@ function App() {
|
||||
});
|
||||
|
||||
break;
|
||||
case "375":
|
||||
case "372":
|
||||
case "376":
|
||||
case 324: {
|
||||
// RPL_CHANNELMODEIS — channel, mode string.
|
||||
if (Array.isArray(msg.params) && msg.params.length >= 2) {
|
||||
addMessage("Server", {
|
||||
...base,
|
||||
text: `${msg.params[0]} modes: ${msg.params[1]}`,
|
||||
system: true,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 375:
|
||||
case 372:
|
||||
case 376:
|
||||
addMessage("Server", { ...base, text: body, system: true });
|
||||
|
||||
break;
|
||||
@@ -637,6 +672,14 @@ function App() {
|
||||
} catch (err) {
|
||||
if (!alive) break;
|
||||
if (err.name === "AbortError") continue;
|
||||
// 401: the session is gone (QUIT from another client, or
|
||||
// expiry). Reloading drops this page's state and brings
|
||||
// back the login screen.
|
||||
if (err.status === 401) {
|
||||
window.location.reload();
|
||||
|
||||
return;
|
||||
}
|
||||
setConnected(false);
|
||||
await new Promise((r) => setTimeout(r, RECONNECT_DELAY));
|
||||
}
|
||||
@@ -972,27 +1015,22 @@ function App() {
|
||||
|
||||
break;
|
||||
}
|
||||
// With no arguments MODE is a query. A change is one sign and
|
||||
// letter, then its value where it takes one: +m, +o nick.
|
||||
const modeArgs = parts.slice(1);
|
||||
if (modeArgs.length > 0) {
|
||||
try {
|
||||
await api("/messages", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
command: "MODE",
|
||||
to: tab.name,
|
||||
params: modeArgs,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
addSystemMessage(
|
||||
"Server",
|
||||
`Mode change failed: ${err.data?.error || "error"}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const body =
|
||||
modeArgs.length > 0
|
||||
? { command: "MODE", to: tab.name, body: modeArgs }
|
||||
: { command: "MODE", to: tab.name };
|
||||
try {
|
||||
await api("/messages", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
} catch (err) {
|
||||
addSystemMessage(
|
||||
"Server",
|
||||
"Usage: /mode <+/-mode> [params]",
|
||||
`MODE failed: ${err.data?.error || "error"}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1009,7 +1047,6 @@ function App() {
|
||||
} catch (e) {
|
||||
// Ignore.
|
||||
}
|
||||
localStorage.removeItem("neoirc_token");
|
||||
localStorage.removeItem("neoirc_channels");
|
||||
window.location.reload();
|
||||
|
||||
@@ -1128,7 +1165,7 @@ function App() {
|
||||
" /me action — Send an action",
|
||||
" /nick newnick — Change your nickname",
|
||||
" /topic [text] — View or set channel topic",
|
||||
" /mode +/-flags — Set channel modes",
|
||||
" /mode [+/-flag [arg]] — View or set channel modes",
|
||||
" /motd — Display the message of the day",
|
||||
" /list — List all channels",
|
||||
" /who [#channel] — List users in a channel",
|
||||
|
||||
Reference in New Issue
Block a user