diff --git a/components/app.js b/components/app.js
index 426ed92..cd304ac 100644
--- a/components/app.js
+++ b/components/app.js
@@ -9,6 +9,8 @@ import JoinForm from "./join-form.js";
import Help from "./help.js";
import NetworkForm from "./network-form.js";
import AuthForm from "./auth-form.js";
+import RegisterForm from "./register-form.js";
+import VerifyForm from "./verify-form.js";
import Composer from "./composer.js";
import ScrollManager from "./scroll-manager.js";
import Dialog from "./dialog.js";
@@ -193,6 +195,8 @@ export default class App extends Component {
this.handleNetworkRemove = this.handleNetworkRemove.bind(this);
this.handleDismissError = this.handleDismissError.bind(this);
this.handleAuthSubmit = this.handleAuthSubmit.bind(this);
+ this.handleRegisterSubmit = this.handleRegisterSubmit.bind(this);
+ this.handleVerifySubmit = this.handleVerifySubmit.bind(this);
this.saveReceipts = debounce(this.saveReceipts.bind(this), 500);
@@ -1403,6 +1407,7 @@ export default class App extends Component {
handleAuthSubmit(username, password) {
let serverID = State.getActiveServerID(this.state);
let client = this.clients.get(serverID);
+ // TODO: show auth status (pending/error) in dialog
client.authenticate("PLAIN", { username, password }).then(() => {
let firstClient = this.clients.values().next().value;
if (client !== firstClient) {
@@ -1424,6 +1429,51 @@ export default class App extends Component {
this.dismissDialog();
}
+ handleRegisterClick(serverID) {
+ let client = this.clients.get(serverID);
+ let emailRequired = client.checkAccountRegistrationCap("email-required");
+ this.openDialog("register", { emailRequired });
+ }
+
+ handleRegisterSubmit(email, password) {
+ let serverID = State.getActiveServerID(this.state);
+ let client = this.clients.get(serverID);
+ // TODO: show registration status (pending/error) in dialog
+ client.registerAccount(email, password).then((data) => {
+ this.dismissDialog();
+
+ if (data.verificationRequired) {
+ this.openDialog("verify", data);
+ }
+
+ let firstClient = this.clients.values().next().value;
+ if (client !== firstClient) {
+ return;
+ }
+
+ let autoconnect = store.autoconnect.load();
+ if (!autoconnect) {
+ return;
+ }
+
+ console.log("Saving account registration credentials");
+ autoconnect = {
+ ...autoconnect,
+ saslPlain: { username: data.account, password },
+ };
+ store.autoconnect.put(autoconnect);
+ });
+ }
+
+ handleVerifySubmit(code) {
+ let serverID = State.getActiveServerID(this.state);
+ let client = this.clients.get(serverID);
+ // TODO: display verification status (pending/error) in dialog
+ client.verifyAccount(this.state.dialogData.account, code).then(() => {
+ this.dismissDialog();
+ });
+ }
+
handleAddNetworkClick() {
this.openDialog("network");
}
@@ -1614,6 +1664,19 @@ export default class App extends Component {
>
`;
break;
+ case "register":
+ dialog = html`
+ <${Dialog} title="Register a new ${getServerName(activeServer, activeBouncerNetwork, isBouncer)} account" onDismiss=${this.dismissDialog}>
+ <${RegisterForm} emailRequired=${dialogData.emailRequired} onSubmit=${this.handleRegisterSubmit}/>
+ >
+ `;
+ break;
+ case "verify":
+ dialog = html`
+ <${Dialog} title="Verify ${getServerName(activeServer, activeBouncerNetwork, isBouncer)} account" onDismiss=${this.dismissDialog}>
+ <${VerifyForm} account=${dialogData.account} message=${dialogData.message} onSubmit=${this.handleVerifySubmit}/>
+ >
+ `;
}
let error = null;
@@ -1669,9 +1732,11 @@ export default class App extends Component {
buffer=${activeBuffer}
server=${activeServer}
isBouncer=${isBouncer}
+ bouncerNetwork=${activeBouncerNetwork}
onChannelClick=${this.handleChannelClick}
onNickClick=${this.handleNickClick}
onAuthClick=${() => this.handleAuthClick(activeBuffer.server)}
+ onRegisterClick=${() => this.handleRegisterClick(activeBuffer.server)}
/>
>
diff --git a/components/buffer.js b/components/buffer.js
index 310654f..bf4a671 100644
--- a/components/buffer.js
+++ b/components/buffer.js
@@ -96,7 +96,7 @@ class LogLine extends Component {
let lineClass = "";
let content;
- let invitee, target;
+ let invitee, target, account;
switch (msg.command) {
case "NOTICE":
case "PRIVMSG":
@@ -205,12 +205,28 @@ class LogLine extends Component {
content = linkify(stripANSI(msg.params[1]), onChannelClick);
break;
case irc.RPL_LOGGEDIN:
- let account = msg.params[2];
+ account = msg.params[2];
content = html`You are now authenticated as ${account}`;
break;
case irc.RPL_LOGGEDOUT:
content = html`You are now unauthenticated`;
break;
+ case "REGISTER":
+ account = msg.params[1];
+ let reason = linkify(msg.params[2]);
+ switch (msg.params[0]) {
+ case "SUCCESS":
+ content = html`A new account has been created, you are now authenticated as ${account}`;
+ break;
+ case "VERIFICATION_REQUIRED":
+ content = html`A new account has been created, but further action is required to complete registration: ${reason}`;
+ break;
+ }
+ break;
+ case "VERIFY":
+ account = msg.params[1];
+ content = html`The new account has been verified, you are now authenticated as ${account}`;
+ break;
case irc.RPL_UMODEIS:
let mode = msg.params[1];
if (mode) {
@@ -233,6 +249,10 @@ class LogLine extends Component {
content = html`${msg.command} ${linkify(msg.params.join(" "))}`;
}
+ if (!content) {
+ return null;
+ }
+
return html`
<${Timestamp} date=${new Date(msg.tags.time)} url=${getMessageURL(buf, msg)}/>
@@ -457,22 +477,37 @@ class ProtocolHandlerNagger extends Component {
}
}
-function AuthNagger({ server, onClick }) {
+function AccountNagger({ server, onAuthClick, onRegisterClick }) {
let accDesc = "an account on this server";
if (server.isupport.has("NETWORK")) {
accDesc = "a " + server.isupport.get("NETWORK") + " account";
}
- function handleClick(event) {
+ function handleAuthClick(event) {
event.preventDefault();
- onClick();
+ onAuthClick();
+ }
+ function handleRegisterClick(event) {
+ event.preventDefault();
+ onRegisterClick();
+ }
+
+ let msg = [html`
+ You are unauthenticated on this server,
+ ${" "}
+
login
+ ${" "}
+ `];
+
+ if (server.supportsAccountRegistration) {
+ msg.push(html`or
register ${accDesc}`);
+ } else {
+ msg.push(html`if you have ${accDesc}`);
}
return html`
- <${Timestamp}/>
- ${" "}
- You are unauthenticated on this server,
login if you have ${accDesc}
+ <${Timestamp}/> ${msg}
`;
}
@@ -515,21 +550,29 @@ export default class Buffer extends Component {
render() {
let buf = this.props.buffer;
- let server = this.props.server;
if (!buf) {
return null;
}
+ let server = this.props.server;
+ let bouncerNetwork = this.props.bouncerNetwork;
+ let serverName = server.isupport.get("NETWORK");
+
let children = [];
if (buf.type == BufferType.SERVER) {
children.push(html`<${NotificationNagger}/>`);
}
if (buf.type == BufferType.SERVER && this.props.isBouncer && !server.isupport.has("BOUNCER_NETID")) {
- let name = server.isupport.get("NETWORK");
- children.push(html`<${ProtocolHandlerNagger} bouncerName=${name}/>`);
+ children.push(html`<${ProtocolHandlerNagger} bouncerName=${serverName}/>`);
}
if (buf.type == BufferType.SERVER && server.status == ServerStatus.REGISTERED && server.supportsSASLPlain && !server.account) {
- children.push(html`<${AuthNagger} server=${server} onClick=${this.props.onAuthClick}/>`);
+ children.push(html`
+ <${AccountNagger}
+ server=${server}
+ onAuthClick=${this.props.onAuthClick}
+ onRegisterClick=${this.props.onRegisterClick}
+ />
+ `);
}
let onChannelClick = this.props.onChannelClick;
diff --git a/components/register-form.js b/components/register-form.js
new file mode 100644
index 0000000..1c43395
--- /dev/null
+++ b/components/register-form.js
@@ -0,0 +1,54 @@
+import { html, Component } from "../lib/index.js";
+
+export default class RegisterForm extends Component {
+ state = {
+ email: "",
+ password: "",
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ }
+
+ handleChange(event) {
+ let target = event.target;
+ let value = target.type == "checkbox" ? target.checked : target.value;
+ this.setState({ [target.name]: value });
+ }
+
+ handleSubmit(event) {
+ event.preventDefault();
+
+ this.props.onSubmit(this.state.email, this.state.password);
+ }
+
+ render() {
+ return html`
+
+ `;
+ }
+}
diff --git a/components/verify-form.js b/components/verify-form.js
new file mode 100644
index 0000000..8589c3d
--- /dev/null
+++ b/components/verify-form.js
@@ -0,0 +1,43 @@
+import { html, Component } from "../lib/index.js";
+import linkify from "../lib/linkify.js";
+
+export default class RegisterForm extends Component {
+ state = {
+ code: "",
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ }
+
+ handleChange(event) {
+ let target = event.target;
+ let value = target.type == "checkbox" ? target.checked : target.value;
+ this.setState({ [target.name]: value });
+ }
+
+ handleSubmit(event) {
+ event.preventDefault();
+
+ this.props.onSubmit(this.state.code);
+ }
+
+ render() {
+ return html`
+
+ `;
+ }
+}
diff --git a/lib/client.js b/lib/client.js
index 2353a74..c4f1beb 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -17,6 +17,7 @@ const permanentCaps = [
"server-time",
"setname",
+ "draft/account-registration",
"draft/chathistory",
"draft/event-playback",
"draft/extended-monitor",
@@ -552,6 +553,14 @@ export default class Client extends EventTarget {
return saslCap.split(",").includes(mech);
}
+ checkAccountRegistrationCap(k) {
+ let v = this.availableCaps["draft/account-registration"];
+ if (v === undefined) {
+ return false;
+ }
+ return v.split(",").includes(k);
+ }
+
requestCaps() {
let wantCaps = [].concat(permanentCaps);
if (!this.params.bouncerNetwork) {
@@ -915,4 +924,45 @@ export default class Client extends EventTarget {
}
});
}
+
+ registerAccount(email, password) {
+ let msg = {
+ command: "REGISTER",
+ params: ["*", email || "*", password],
+ };
+ return this.roundtrip(msg, (msg) => {
+ switch (msg.command) {
+ case "REGISTER":
+ let result = msg.params[0];
+ return {
+ verificationRequired: result === "VERIFICATION_REQUIRED",
+ account: msg.params[1],
+ message: msg.params[2],
+ };
+ case "FAIL":
+ if (msg.params[0] === "REGISTER") {
+ throw msg;
+ }
+ break;
+ }
+ });
+ }
+
+ verifyAccount(account, code) {
+ let msg = {
+ command: "VERIFY",
+ params: [account, code],
+ };
+ return this.roundtrip(msg, (msg) => {
+ switch (msg.command) {
+ case "VERIFY":
+ return { message: msg.params[2] };
+ case "FAIL":
+ if (msg.params[0] === "VERIFY") {
+ throw msg;
+ }
+ break;
+ }
+ });
+ }
}
diff --git a/state.js b/state.js
index 33b3142..fb3507e 100644
--- a/state.js
+++ b/state.js
@@ -257,6 +257,7 @@ export const State = {
users: new irc.CaseMapMap(null, irc.CaseMapping.RFC1459),
account: null,
supportsSASLPlain: false,
+ supportsAccountRegistration: false,
});
return [id, { servers }];
},
@@ -348,11 +349,20 @@ export const State = {
}),
};
case "CAP":
- return updateServer({ supportsSASLPlain: client.supportsSASL("PLAIN") });
+ return updateServer({
+ supportsSASLPlain: client.supportsSASL("PLAIN"),
+ supportsAccountRegistration: !!client.enabledCaps["draft/account-registration"],
+ });
case irc.RPL_LOGGEDIN:
return updateServer({ account: msg.params[2] });
case irc.RPL_LOGGEDOUT:
return updateServer({ account: null });
+ case "REGISTER":
+ case "VERIFY":
+ if (msg.params[0] === "SUCCESS") {
+ return updateServer({ account: msg.params[1] });
+ }
+ break;
case irc.RPL_NOTOPIC:
channel = msg.params[1];
return updateBuffer(channel, { topic: null });