Bring the web client in line with the current API (closes #145) #175
+88
-51
@@ -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 = {}) {
|
function api(path, opts = {}) {
|
||||||
const token = localStorage.getItem("neoirc_token");
|
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(opts.headers || {}),
|
...(opts.headers || {}),
|
||||||
};
|
};
|
||||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
||||||
const { signal, ...rest } = opts;
|
const { signal, ...rest } = opts;
|
||||||
|
|
||||||
return fetch(API + path, { ...rest, headers, signal }).then(async (r) => {
|
return fetch(API + path, { ...rest, headers, signal }).then(async (r) => {
|
||||||
@@ -122,12 +122,11 @@ function LoginScreen({ onLogin }) {
|
|||||||
if (s.name) hashcashResourceRef.current = s.name;
|
if (s.name) hashcashResourceRef.current = s.name;
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
const saved = localStorage.getItem("neoirc_token");
|
// Resume the session if the browser still holds a valid auth
|
||||||
if (saved) {
|
// cookie; a 401 leaves the login form up.
|
||||||
api("/state?initChannelState=1")
|
api("/state?initChannelState=1")
|
||||||
.then((u) => onLogin(u.nick, true))
|
.then((u) => onLogin(u.nick, true))
|
||||||
.catch(() => localStorage.removeItem("neoirc_token"));
|
.catch(() => {});
|
||||||
}
|
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -152,7 +151,6 @@ function LoginScreen({ onLogin }) {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(reqBody),
|
body: JSON.stringify(reqBody),
|
||||||
});
|
});
|
||||||
localStorage.setItem("neoirc_token", res.token);
|
|
||||||
onLogin(res.nick);
|
onLogin(res.nick);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.data?.error || "Connection failed");
|
setError(err.data?.error || "Connection failed");
|
||||||
@@ -227,10 +225,9 @@ function UserList({ members, onNickClick }) {
|
|||||||
const regular = [];
|
const regular = [];
|
||||||
|
|
||||||
for (const m of members) {
|
for (const m of members) {
|
||||||
const mode = m.mode || "";
|
if (m.isOperator) {
|
||||||
if (mode === "o") {
|
|
||||||
ops.push(m);
|
ops.push(m);
|
||||||
} else if (mode === "v") {
|
} else if (m.isVoiced) {
|
||||||
voiced.push(m);
|
voiced.push(m);
|
||||||
} else {
|
} else {
|
||||||
regular.push(m);
|
regular.push(m);
|
||||||
@@ -366,7 +363,9 @@ function App() {
|
|||||||
command: msg.command,
|
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 "PRIVMSG":
|
||||||
case "NOTICE": {
|
case "NOTICE": {
|
||||||
let text = body;
|
let text = body;
|
||||||
@@ -464,8 +463,9 @@ function App() {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "353": {
|
case 353: {
|
||||||
// RPL_NAMREPLY — parse mode prefixes from names list.
|
// RPL_NAMREPLY — each name is a hostmask (nick!user@host),
|
||||||
|
// prefixed with @ for an operator or + for a voiced member.
|
||||||
if (
|
if (
|
||||||
Array.isArray(msg.params) &&
|
Array.isArray(msg.params) &&
|
||||||
msg.params.length >= 2 &&
|
msg.params.length >= 2 &&
|
||||||
@@ -477,21 +477,18 @@ function App() {
|
|||||||
: String(msg.body);
|
: String(msg.body);
|
||||||
const names = namesStr.split(/\s+/).filter(Boolean);
|
const names = namesStr.split(/\s+/).filter(Boolean);
|
||||||
const parsed = names.map((n) => {
|
const parsed = names.map((n) => {
|
||||||
if (n.startsWith("@")) {
|
const isOperator = n.startsWith("@");
|
||||||
return { nick: n.slice(1), mode: "o" };
|
const isVoiced = n.startsWith("+");
|
||||||
}
|
const hostmask = isOperator || isVoiced ? n.slice(1) : n;
|
||||||
if (n.startsWith("+")) {
|
|
||||||
return { nick: n.slice(1), mode: "v" };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { nick: n, mode: "" };
|
return { nick: hostmask.split("!")[0], isOperator, isVoiced };
|
||||||
});
|
});
|
||||||
setMembers((prev) => ({ ...prev, [channel]: parsed }));
|
setMembers((prev) => ({ ...prev, [channel]: parsed }));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "332": {
|
case 332: {
|
||||||
// RPL_TOPIC — set topic from server.
|
// RPL_TOPIC — set topic from server.
|
||||||
if (Array.isArray(msg.params) && msg.params.length >= 1) {
|
if (Array.isArray(msg.params) && msg.params.length >= 1) {
|
||||||
const channel = msg.params[0];
|
const channel = msg.params[0];
|
||||||
@@ -503,7 +500,7 @@ function App() {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "322": {
|
case 322: {
|
||||||
// RPL_LIST — channel, member count, topic.
|
// RPL_LIST — channel, member count, topic.
|
||||||
if (Array.isArray(msg.params) && msg.params.length >= 2) {
|
if (Array.isArray(msg.params) && msg.params.length >= 2) {
|
||||||
const chName = msg.params[0];
|
const chName = msg.params[0];
|
||||||
@@ -518,7 +515,7 @@ function App() {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "323":
|
case 323:
|
||||||
addMessage("Server", {
|
addMessage("Server", {
|
||||||
...base,
|
...base,
|
||||||
text: body || "End of channel list",
|
text: body || "End of channel list",
|
||||||
@@ -526,7 +523,7 @@ function App() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "352": {
|
case 352: {
|
||||||
// RPL_WHOREPLY — channel, user, host, server, nick, flags.
|
// RPL_WHOREPLY — channel, user, host, server, nick, flags.
|
||||||
if (Array.isArray(msg.params) && msg.params.length >= 5) {
|
if (Array.isArray(msg.params) && msg.params.length >= 5) {
|
||||||
const whoCh = msg.params[0];
|
const whoCh = msg.params[0];
|
||||||
@@ -541,7 +538,7 @@ function App() {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "315":
|
case 315:
|
||||||
addMessage("Server", {
|
addMessage("Server", {
|
||||||
...base,
|
...base,
|
||||||
text: body || "End of /WHO list",
|
text: body || "End of /WHO list",
|
||||||
@@ -549,20 +546,20 @@ function App() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "311": {
|
case 311: {
|
||||||
// RPL_WHOISUSER — nick, user, host, *, realname.
|
// RPL_WHOISUSER — params nick, user, host, *; body realname.
|
||||||
if (Array.isArray(msg.params) && msg.params.length >= 1) {
|
if (Array.isArray(msg.params) && msg.params.length >= 3) {
|
||||||
const wiNick = msg.params[0];
|
const [wiNick, wiUser, wiHost] = msg.params;
|
||||||
addMessage("Server", {
|
addMessage("Server", {
|
||||||
...base,
|
...base,
|
||||||
text: `${wiNick} (${body})`,
|
text: `${wiNick} is ${wiUser}@${wiHost} (${body})`,
|
||||||
system: true,
|
system: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "312": {
|
case 312: {
|
||||||
// RPL_WHOISSERVER — nick, server, server info.
|
// RPL_WHOISSERVER — nick, server, server info.
|
||||||
if (Array.isArray(msg.params) && msg.params.length >= 2) {
|
if (Array.isArray(msg.params) && msg.params.length >= 2) {
|
||||||
const wiNick = msg.params[0];
|
const wiNick = msg.params[0];
|
||||||
@@ -576,7 +573,33 @@ function App() {
|
|||||||
|
|
||||||
break;
|
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.
|
// RPL_WHOISCHANNELS — nick, channels.
|
||||||
if (Array.isArray(msg.params) && msg.params.length >= 1) {
|
if (Array.isArray(msg.params) && msg.params.length >= 1) {
|
||||||
const wiNick = msg.params[0];
|
const wiNick = msg.params[0];
|
||||||
@@ -589,7 +612,7 @@ function App() {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "318":
|
case 318:
|
||||||
addMessage("Server", {
|
addMessage("Server", {
|
||||||
...base,
|
...base,
|
||||||
text: body || "End of /WHOIS list",
|
text: body || "End of /WHOIS list",
|
||||||
@@ -597,9 +620,21 @@ function App() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "375":
|
case 324: {
|
||||||
case "372":
|
// RPL_CHANNELMODEIS — channel, mode string.
|
||||||
case "376":
|
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 });
|
addMessage("Server", { ...base, text: body, system: true });
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -637,6 +672,14 @@ function App() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!alive) break;
|
if (!alive) break;
|
||||||
if (err.name === "AbortError") continue;
|
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);
|
setConnected(false);
|
||||||
await new Promise((r) => setTimeout(r, RECONNECT_DELAY));
|
await new Promise((r) => setTimeout(r, RECONNECT_DELAY));
|
||||||
}
|
}
|
||||||
@@ -972,27 +1015,22 @@ function App() {
|
|||||||
|
|
||||||
break;
|
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);
|
const modeArgs = parts.slice(1);
|
||||||
if (modeArgs.length > 0) {
|
const body =
|
||||||
|
modeArgs.length > 0
|
||||||
|
? { command: "MODE", to: tab.name, body: modeArgs }
|
||||||
|
: { command: "MODE", to: tab.name };
|
||||||
try {
|
try {
|
||||||
await api("/messages", {
|
await api("/messages", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(body),
|
||||||
command: "MODE",
|
|
||||||
to: tab.name,
|
|
||||||
params: modeArgs,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
addSystemMessage(
|
addSystemMessage(
|
||||||
"Server",
|
"Server",
|
||||||
`Mode change failed: ${err.data?.error || "error"}`,
|
`MODE failed: ${err.data?.error || "error"}`,
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
addSystemMessage(
|
|
||||||
"Server",
|
|
||||||
"Usage: /mode <+/-mode> [params]",
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1009,7 +1047,6 @@ function App() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore.
|
// Ignore.
|
||||||
}
|
}
|
||||||
localStorage.removeItem("neoirc_token");
|
|
||||||
localStorage.removeItem("neoirc_channels");
|
localStorage.removeItem("neoirc_channels");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
|
||||||
@@ -1128,7 +1165,7 @@ function App() {
|
|||||||
" /me action — Send an action",
|
" /me action — Send an action",
|
||||||
" /nick newnick — Change your nickname",
|
" /nick newnick — Change your nickname",
|
||||||
" /topic [text] — View or set channel topic",
|
" /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",
|
" /motd — Display the message of the day",
|
||||||
" /list — List all channels",
|
" /list — List all channels",
|
||||||
" /who [#channel] — List users in a channel",
|
" /who [#channel] — List users in a channel",
|
||||||
|
|||||||
Reference in New Issue
Block a user