modules: split services and the ircd link into external crates
nickserv, chanserv and the InspIRCd protocol move out of the binary into their own workspace crates (fedserv-nickserv, fedserv-chanserv, fedserv-inspircd), each depending only on fedserv-api. human_time and the branded account emails move into the SDK crate so a module needs nothing from core; the engine keeps its own inherent methods and builds emails via fedserv-api too. The bin now constructs each module from its crate instead of an in-tree #[path] include. Proves the SDK is self-sufficient: a third-party module is the same shape.
This commit is contained in:
parent
8ed1a9ab70
commit
596630df53
52 changed files with 197 additions and 162 deletions
8
nickserv/Cargo.toml
Normal file
8
nickserv/Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "fedserv-nickserv"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
description = "NickServ account-registration service module for fedserv."
|
||||
|
||||
[dependencies]
|
||||
fedserv-api = { path = "../api" }
|
||||
27
nickserv/src/alist.rs
Normal file
27
nickserv/src/alist.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use fedserv_api::Store;
|
||||
use fedserv_api::{Sender, ServiceCtx};
|
||||
|
||||
// ALIST: list the channels the sender's account founds or has access on.
|
||||
pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
|
||||
let Some(account) = from.account else {
|
||||
ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first.");
|
||||
return;
|
||||
};
|
||||
let mut rows: Vec<(String, &str)> = Vec::new();
|
||||
for c in db.channels() {
|
||||
if c.founder.eq_ignore_ascii_case(account) {
|
||||
rows.push((c.name.clone(), "founder"));
|
||||
} else if let Some(a) = c.access.iter().find(|a| a.account.eq_ignore_ascii_case(account)) {
|
||||
rows.push((c.name.clone(), if a.level == "voice" { "voice" } else { "op" }));
|
||||
}
|
||||
}
|
||||
if rows.is_empty() {
|
||||
ctx.notice(me, from.uid, "You have access on no channels.");
|
||||
return;
|
||||
}
|
||||
rows.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
ctx.notice(me, from.uid, format!("Channels you have access on ({}):", rows.len()));
|
||||
for (chan, role) in rows {
|
||||
ctx.notice(me, from.uid, format!(" \x02{chan}\x02 ({role})"));
|
||||
}
|
||||
}
|
||||
59
nickserv/src/cert.rs
Normal file
59
nickserv/src/cert.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use fedserv_api::{CertError, Store};
|
||||
use fedserv_api::{Sender, ServiceCtx};
|
||||
|
||||
// CERT ADD|DEL|LIST <password> [fingerprint]: manage the TLS certificate
|
||||
// fingerprints that may log in to your account via SASL EXTERNAL. Each
|
||||
// subcommand is password-gated, so it needs no identified-session state.
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||
let auth = |db: &dyn Store, password: &str| db.authenticate(from.nick, password).map(str::to_string);
|
||||
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||
Some("LIST") => {
|
||||
let Some(password) = args.get(2) else {
|
||||
ctx.notice(me, from.uid, "Syntax: CERT LIST <password>");
|
||||
return;
|
||||
};
|
||||
let Some(account) = auth(db, password) else {
|
||||
ctx.notice(me, from.uid, "Invalid password.");
|
||||
return;
|
||||
};
|
||||
let fps = db.certfps(&account);
|
||||
if fps.is_empty() {
|
||||
ctx.notice(me, from.uid, "No certificate fingerprints are registered to your account.");
|
||||
} else {
|
||||
ctx.notice(me, from.uid, format!("Registered fingerprints: {}", fps.join(", ")));
|
||||
}
|
||||
}
|
||||
Some("ADD") => {
|
||||
let (Some(password), Some(fp)) = (args.get(2), args.get(3)) else {
|
||||
ctx.notice(me, from.uid, "Syntax: CERT ADD <password> <fingerprint>");
|
||||
return;
|
||||
};
|
||||
let Some(account) = auth(db, password) else {
|
||||
ctx.notice(me, from.uid, "Invalid password.");
|
||||
return;
|
||||
};
|
||||
match db.certfp_add(&account, fp) {
|
||||
Ok(()) => ctx.notice(me, from.uid, format!("Added fingerprint \x02{fp}\x02. You can now log in with SASL EXTERNAL.")),
|
||||
Err(CertError::InUse) => ctx.notice(me, from.uid, "That fingerprint is already registered."),
|
||||
Err(CertError::Invalid) => ctx.notice(me, from.uid, "That does not look like a certificate fingerprint."),
|
||||
Err(CertError::NoAccount | CertError::Internal) => ctx.notice(me, from.uid, "Could not add the fingerprint, please try again later."),
|
||||
}
|
||||
}
|
||||
Some("DEL") | Some("REMOVE") => {
|
||||
let (Some(password), Some(fp)) = (args.get(2), args.get(3)) else {
|
||||
ctx.notice(me, from.uid, "Syntax: CERT DEL <password> <fingerprint>");
|
||||
return;
|
||||
};
|
||||
let Some(account) = auth(db, password) else {
|
||||
ctx.notice(me, from.uid, "Invalid password.");
|
||||
return;
|
||||
};
|
||||
match db.certfp_del(&account, fp) {
|
||||
Ok(true) => ctx.notice(me, from.uid, format!("Removed fingerprint \x02{fp}\x02.")),
|
||||
Ok(false) => ctx.notice(me, from.uid, "No such fingerprint is registered to your account."),
|
||||
Err(_) => ctx.notice(me, from.uid, "Could not remove the fingerprint, please try again later."),
|
||||
}
|
||||
}
|
||||
_ => ctx.notice(me, from.uid, "Syntax: CERT ADD|DEL|LIST <password> [fingerprint]"),
|
||||
}
|
||||
}
|
||||
26
nickserv/src/confirm.rs
Normal file
26
nickserv/src/confirm.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use fedserv_api::{CodeKind, Store};
|
||||
use fedserv_api::{Sender, ServiceCtx};
|
||||
|
||||
// CONFIRM <code>: confirm your account's email with the code you were emailed.
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||
let Some(&code) = args.get(1) else {
|
||||
ctx.notice(me, from.uid, "Syntax: CONFIRM <code>");
|
||||
return;
|
||||
};
|
||||
let Some(account) = from.account.map(str::to_string).or_else(|| db.resolve_account(from.nick).map(str::to_string)) else {
|
||||
ctx.notice(me, from.uid, "You don't have an account to confirm.");
|
||||
return;
|
||||
};
|
||||
if db.is_verified(&account) {
|
||||
ctx.notice(me, from.uid, format!("\x02{account}\x02 is already confirmed."));
|
||||
return;
|
||||
}
|
||||
if !db.take_code(&account, CodeKind::Confirm, code) {
|
||||
ctx.notice(me, from.uid, "Invalid or expired confirmation code.");
|
||||
return;
|
||||
}
|
||||
match db.verify_account(&account) {
|
||||
Ok(()) => ctx.notice(me, from.uid, format!("\x02{account}\x02 is now confirmed. Thanks!")),
|
||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||
}
|
||||
}
|
||||
33
nickserv/src/drop.rs
Normal file
33
nickserv/src/drop.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use fedserv_api::Store;
|
||||
use fedserv_api::{Sender, ServiceCtx};
|
||||
use fedserv_api::NetView;
|
||||
|
||||
// DROP <password>: delete your account. Re-authenticates as confirmation, releases
|
||||
// and drops the channels you found, and logs you out.
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &mut dyn Store) {
|
||||
let Some(account) = from.account else {
|
||||
ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first.");
|
||||
return;
|
||||
};
|
||||
let Some(&password) = args.get(1) else {
|
||||
ctx.notice(me, from.uid, "Syntax: DROP <password>");
|
||||
return;
|
||||
};
|
||||
if db.authenticate(account, password).is_none() {
|
||||
ctx.notice(me, from.uid, "Invalid password.");
|
||||
return;
|
||||
}
|
||||
let channels = db.channels_owned_by(account);
|
||||
for chan in &channels {
|
||||
let _ = db.drop_channel(chan);
|
||||
ctx.channel_mode("", chan, "-r"); // server-sourced: release the registered mode
|
||||
}
|
||||
let _ = db.drop_account(account);
|
||||
for uid in net.uids_logged_into(account) {
|
||||
ctx.logout(&uid);
|
||||
}
|
||||
ctx.notice(me, from.uid, format!("Your account \x02{account}\x02 has been dropped."));
|
||||
if !channels.is_empty() {
|
||||
ctx.notice(me, from.uid, format!("Channels released: {}.", channels.join(", ")));
|
||||
}
|
||||
}
|
||||
33
nickserv/src/ghost.rs
Normal file
33
nickserv/src/ghost.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use fedserv_api::Store;
|
||||
use fedserv_api::{Sender, ServiceCtx};
|
||||
use fedserv_api::NetView;
|
||||
|
||||
// GHOST/RECOVER <nick> [password]: rename off a session using a nick you own,
|
||||
// either by being identified to its account or giving that account's password.
|
||||
pub fn handle(me: &str, guest_nick: &str, guest_seq: &mut u32, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) {
|
||||
let Some(&target) = args.get(1) else {
|
||||
ctx.notice(me, from.uid, "Syntax: GHOST <nick> [password]");
|
||||
return;
|
||||
};
|
||||
let Some(account) = db.resolve_account(target).map(str::to_string) else {
|
||||
ctx.notice(me, from.uid, format!("\x02{target}\x02 isn't registered."));
|
||||
return;
|
||||
};
|
||||
let owns = from.account == Some(account.as_str()) || args.get(2).is_some_and(|pw| db.authenticate(target, pw).is_some());
|
||||
if !owns {
|
||||
ctx.notice(me, from.uid, format!("Access denied. Identify to \x02{account}\x02 or give its password."));
|
||||
return;
|
||||
}
|
||||
let Some(ghost) = net.uid_by_nick(target).map(str::to_string) else {
|
||||
ctx.notice(me, from.uid, format!("Nobody is using \x02{target}\x02."));
|
||||
return;
|
||||
};
|
||||
if ghost == from.uid {
|
||||
ctx.notice(me, from.uid, "That's you.");
|
||||
return;
|
||||
}
|
||||
let guest = format!("{guest_nick}{guest_seq}");
|
||||
*guest_seq = guest_seq.wrapping_add(1);
|
||||
ctx.force_nick(&ghost, &guest);
|
||||
ctx.notice(me, from.uid, format!("\x02{target}\x02 has been freed."));
|
||||
}
|
||||
17
nickserv/src/glist.rs
Normal file
17
nickserv/src/glist.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use fedserv_api::Store;
|
||||
use fedserv_api::{Sender, ServiceCtx};
|
||||
|
||||
// GLIST: list the nicks grouped to your account.
|
||||
pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
|
||||
let Some(account) = from.account else {
|
||||
ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first.");
|
||||
return;
|
||||
};
|
||||
let mut nicks = db.grouped_nicks(account);
|
||||
nicks.sort();
|
||||
ctx.notice(me, from.uid, format!("Nicks grouped to \x02{account}\x02:"));
|
||||
ctx.notice(me, from.uid, format!(" \x02{account}\x02 (main)"));
|
||||
for n in nicks {
|
||||
ctx.notice(me, from.uid, format!(" \x02{n}\x02"));
|
||||
}
|
||||
}
|
||||
23
nickserv/src/group.rs
Normal file
23
nickserv/src/group.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use fedserv_api::Store;
|
||||
use fedserv_api::{Sender, ServiceCtx};
|
||||
|
||||
// GROUP <account> <password>: link your current nick to an existing account, so
|
||||
// you can identify to it under this nick too.
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||
let (Some(&account), Some(&password)) = (args.get(1), args.get(2)) else {
|
||||
ctx.notice(me, from.uid, "Syntax: GROUP <account> <password>");
|
||||
return;
|
||||
};
|
||||
let Some(canonical) = db.authenticate(account, password).map(str::to_string) else {
|
||||
ctx.notice(me, from.uid, "Invalid account or password.");
|
||||
return;
|
||||
};
|
||||
if db.account(from.nick).is_some() {
|
||||
ctx.notice(me, from.uid, format!("\x02{}\x02 is itself a registered account.", from.nick));
|
||||
return;
|
||||
}
|
||||
match db.group_nick(from.nick, &canonical) {
|
||||
Ok(()) => ctx.notice(me, from.uid, format!("Your nick \x02{}\x02 is now grouped to \x02{canonical}\x02.", from.nick)),
|
||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||
}
|
||||
}
|
||||
33
nickserv/src/identify.rs
Normal file
33
nickserv/src/identify.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use fedserv_api::Store;
|
||||
use fedserv_api::{Sender, ServiceCtx};
|
||||
|
||||
// IDENTIFY [account] <password>: log in. The account defaults to the current
|
||||
// nick, so both the bare-password and account+password forms work.
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &dyn Store) {
|
||||
let (account_name, password) = match (args.get(1), args.get(2)) {
|
||||
(Some(account), Some(password)) => (*account, *password),
|
||||
(Some(password), None) => (from.nick, *password),
|
||||
_ => {
|
||||
ctx.notice(me, from.uid, "Syntax: IDENTIFY [account] <password>");
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Distinguish an unregistered account from a wrong password.
|
||||
if !db.exists(account_name) {
|
||||
ctx.notice(me, from.uid, format!("\x02{account_name}\x02 isn't registered."));
|
||||
return;
|
||||
}
|
||||
match db.authenticate(account_name, password) {
|
||||
Some(account) => {
|
||||
// Already identified to this account: don't re-fire the login.
|
||||
if from.account == Some(account) {
|
||||
ctx.notice(me, from.uid, format!("You're already identified as \x02{}\x02.", account));
|
||||
return;
|
||||
}
|
||||
let account = account.to_string();
|
||||
ctx.login(from.uid, &account);
|
||||
ctx.notice(me, from.uid, format!("You're now identified as \x02{}\x02. Welcome back!", account));
|
||||
}
|
||||
None => ctx.notice(me, from.uid, "Invalid password. Please try again."),
|
||||
}
|
||||
}
|
||||
21
nickserv/src/info.rs
Normal file
21
nickserv/src/info.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
use fedserv_api::{human_time, Store};
|
||||
use fedserv_api::{Sender, ServiceCtx};
|
||||
|
||||
// INFO [account]: show an account's registration details. The email is shown
|
||||
// only to the account's own owner.
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &dyn Store) {
|
||||
let name = args.get(1).copied().unwrap_or(from.nick);
|
||||
let Some(acct) = db.account(name) else {
|
||||
ctx.notice(me, from.uid, format!("\x02{name}\x02 isn't registered."));
|
||||
return;
|
||||
};
|
||||
ctx.notice(me, from.uid, format!("Information for \x02{}\x02:", acct.name));
|
||||
ctx.notice(me, from.uid, format!(" Registered : {}", human_time(acct.ts)));
|
||||
if from.account == Some(acct.name.as_str()) {
|
||||
match &acct.email {
|
||||
Some(email) if acct.verified => ctx.notice(me, from.uid, format!(" Email : {email}")),
|
||||
Some(email) => ctx.notice(me, from.uid, format!(" Email : {email} (unconfirmed)")),
|
||||
None => ctx.notice(me, from.uid, " Email : (none set)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
78
nickserv/src/lib.rs
Normal file
78
nickserv/src/lib.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
use fedserv_api::Store;
|
||||
use fedserv_api::{Sender, Service, ServiceCtx};
|
||||
use fedserv_api::NetView;
|
||||
|
||||
#[path = "register.rs"]
|
||||
mod register;
|
||||
#[path = "identify.rs"]
|
||||
mod identify;
|
||||
#[path = "logout.rs"]
|
||||
mod logout;
|
||||
#[path = "cert.rs"]
|
||||
mod cert;
|
||||
#[path = "info.rs"]
|
||||
mod info;
|
||||
#[path = "alist.rs"]
|
||||
mod alist;
|
||||
#[path = "set.rs"]
|
||||
mod set;
|
||||
#[path = "drop.rs"]
|
||||
mod drop;
|
||||
#[path = "group.rs"]
|
||||
mod group;
|
||||
#[path = "glist.rs"]
|
||||
mod glist;
|
||||
#[path = "ungroup.rs"]
|
||||
mod ungroup;
|
||||
#[path = "ghost.rs"]
|
||||
mod ghost;
|
||||
#[path = "resetpass.rs"]
|
||||
mod resetpass;
|
||||
#[path = "confirm.rs"]
|
||||
mod confirm;
|
||||
|
||||
pub struct NickServ {
|
||||
pub uid: String,
|
||||
// Nick prefix assigned on LOGOUT (default "Guest"); a per-session sequence is
|
||||
// appended so successive guests don't collide within a run.
|
||||
pub guest_nick: String,
|
||||
pub guest_seq: u32,
|
||||
}
|
||||
|
||||
impl Service for NickServ {
|
||||
fn nick(&self) -> &str {
|
||||
"NickServ"
|
||||
}
|
||||
fn uid(&self) -> &str {
|
||||
&self.uid
|
||||
}
|
||||
fn gecos(&self) -> &str {
|
||||
"Nickname Services"
|
||||
}
|
||||
fn manages_accounts(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &mut dyn Store) {
|
||||
let me = self.uid.as_str();
|
||||
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||
Some("REGISTER") => register::handle(me, from, args, ctx),
|
||||
Some("IDENTIFY") | Some("ID") => identify::handle(me, from, args, ctx, db),
|
||||
Some("LOGOUT") | Some("LOGOFF") => logout::handle(me, &self.guest_nick, &mut self.guest_seq, from, ctx),
|
||||
Some("CERT") => cert::handle(me, from, args, ctx, db),
|
||||
Some("INFO") => info::handle(me, from, args, ctx, db),
|
||||
Some("ALIST") => alist::handle(me, from, ctx, db),
|
||||
Some("SET") => set::handle(me, from, args, ctx, db),
|
||||
Some("DROP") => drop::handle(me, from, args, ctx, net, db),
|
||||
Some("GROUP") => group::handle(me, from, args, ctx, db),
|
||||
Some("GLIST") => glist::handle(me, from, ctx, db),
|
||||
Some("UNGROUP") => ungroup::handle(me, from, args, ctx, db),
|
||||
Some("GHOST") | Some("RECOVER") => ghost::handle(me, &self.guest_nick, &mut self.guest_seq, from, args, ctx, net, db),
|
||||
Some("RESETPASS") => resetpass::handle(me, from, args, ctx, db),
|
||||
Some("CONFIRM") => confirm::handle(me, from, args, ctx, db),
|
||||
Some("HELP") => ctx.notice(me, from.uid, "NickServ looks after your nickname. Commands: \x02REGISTER\x02 <password> [email], \x02IDENTIFY\x02 [account] <password>, \x02INFO\x02 [account], \x02ALIST\x02, \x02GROUP\x02/\x02GLIST\x02/\x02UNGROUP\x02, \x02GHOST\x02 <nick> [password], \x02SET\x02 PASSWORD|EMAIL, \x02RESETPASS\x02 <account>, \x02CONFIRM\x02 <code>, \x02DROP\x02 <password>, \x02LOGOUT\x02, \x02CERT\x02 ADD|DEL|LIST <password> [fingerprint]."),
|
||||
Some(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
nickserv/src/logout.rs
Normal file
14
nickserv/src/logout.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
use fedserv_api::{Sender, ServiceCtx};
|
||||
|
||||
// LOGOUT: log out and rename to a guest nick (prefix + a per-logout sequence).
|
||||
pub fn handle(me: &str, guest_nick: &str, guest_seq: &mut u32, from: &Sender, ctx: &mut ServiceCtx) {
|
||||
if from.account.is_none() {
|
||||
ctx.notice(me, from.uid, "You're not logged in.");
|
||||
return;
|
||||
}
|
||||
let guest = format!("{guest_nick}{guest_seq}");
|
||||
*guest_seq = guest_seq.wrapping_add(1);
|
||||
ctx.logout(from.uid);
|
||||
ctx.force_nick(from.uid, &guest);
|
||||
ctx.notice(me, from.uid, format!("You're now logged out. Your nick is now \x02{}\x02.", guest));
|
||||
}
|
||||
17
nickserv/src/register.rs
Normal file
17
nickserv/src/register.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use fedserv_api::{Sender, ServiceCtx};
|
||||
use fedserv_api::RegReply;
|
||||
|
||||
// REGISTER <password> [email]: register the sender's current nick. The engine
|
||||
// derives the password off-thread, commits, and answers.
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx) {
|
||||
let Some(password) = args.get(1) else {
|
||||
ctx.notice(me, from.uid, "Syntax: REGISTER <password> [email]");
|
||||
return;
|
||||
};
|
||||
let email = args.get(2).map(|s| s.to_string());
|
||||
ctx.defer_register(from.nick, *password, email, RegReply::NickServ {
|
||||
agent: me.to_string(),
|
||||
uid: from.uid.to_string(),
|
||||
nick: from.nick.to_string(),
|
||||
});
|
||||
}
|
||||
39
nickserv/src/resetpass.rs
Normal file
39
nickserv/src/resetpass.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
use fedserv_api::{CodeKind, Store};
|
||||
use fedserv_api::{Sender, ServiceCtx};
|
||||
|
||||
// RESETPASS <account>: email a reset code to the address on file.
|
||||
// RESETPASS <account> <code> <newpassword>: complete the reset with that code.
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||
if !db.email_enabled() {
|
||||
ctx.notice(me, from.uid, "Password reset by email isn't available here.");
|
||||
return;
|
||||
}
|
||||
match (args.get(1), args.get(2), args.get(3)) {
|
||||
(Some(&name), None, None) => {
|
||||
let Some(canonical) = db.resolve_account(name).map(str::to_string) else {
|
||||
ctx.notice(me, from.uid, format!("\x02{name}\x02 isn't registered."));
|
||||
return;
|
||||
};
|
||||
let Some(email) = db.account(&canonical).and_then(|a| a.email.clone()) else {
|
||||
ctx.notice(me, from.uid, "That account has no email on file, so it can't be reset.");
|
||||
return;
|
||||
};
|
||||
let code = db.issue_code(&canonical, CodeKind::Reset);
|
||||
let mail = fedserv_api::email::reset(db.email_brand(), db.email_accent(), db.email_logo(), &canonical, &code);
|
||||
ctx.send_email(email, mail.subject, mail.text, Some(mail.html));
|
||||
ctx.notice(me, from.uid, format!("A reset code has been emailed to the address on file for \x02{canonical}\x02."));
|
||||
}
|
||||
(Some(&name), Some(&code), Some(&newpass)) => {
|
||||
let Some(canonical) = db.resolve_account(name).map(str::to_string) else {
|
||||
ctx.notice(me, from.uid, format!("\x02{name}\x02 isn't registered."));
|
||||
return;
|
||||
};
|
||||
if !db.take_code(&canonical, CodeKind::Reset, code) {
|
||||
ctx.notice(me, from.uid, "Invalid or expired reset code.");
|
||||
return;
|
||||
}
|
||||
ctx.defer_password(&canonical, newpass, me, from.uid);
|
||||
}
|
||||
_ => ctx.notice(me, from.uid, "Syntax: RESETPASS <account> [<code> <newpassword>]"),
|
||||
}
|
||||
}
|
||||
30
nickserv/src/set.rs
Normal file
30
nickserv/src/set.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use fedserv_api::Store;
|
||||
use fedserv_api::{Sender, ServiceCtx};
|
||||
|
||||
// SET PASSWORD <newpassword> | SET EMAIL [address]: change your account settings.
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||
let Some(account) = from.account else {
|
||||
ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first.");
|
||||
return;
|
||||
};
|
||||
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||
Some("PASSWORD") | Some("PASS") => {
|
||||
let Some(&password) = args.get(2) else {
|
||||
ctx.notice(me, from.uid, "Syntax: SET PASSWORD <newpassword>");
|
||||
return;
|
||||
};
|
||||
// The link layer derives the new password off-thread, then commits it.
|
||||
ctx.defer_password(account, password, me, from.uid);
|
||||
}
|
||||
Some("EMAIL") => {
|
||||
let email = args.get(2).map(|s| s.to_string());
|
||||
let cleared = email.is_none();
|
||||
match db.set_email(account, email) {
|
||||
Ok(()) if cleared => ctx.notice(me, from.uid, format!("Email for \x02{account}\x02 cleared.")),
|
||||
Ok(()) => ctx.notice(me, from.uid, format!("Email for \x02{account}\x02 updated.")),
|
||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||
}
|
||||
}
|
||||
_ => ctx.notice(me, from.uid, "Syntax: SET PASSWORD <newpassword> | SET EMAIL [address]"),
|
||||
}
|
||||
}
|
||||
24
nickserv/src/ungroup.rs
Normal file
24
nickserv/src/ungroup.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use fedserv_api::Store;
|
||||
use fedserv_api::{Sender, ServiceCtx};
|
||||
|
||||
// UNGROUP [nick]: remove a nick grouped to your account (defaults to your current nick).
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||
let Some(account) = from.account else {
|
||||
ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first.");
|
||||
return;
|
||||
};
|
||||
let nick = args.get(1).copied().unwrap_or(from.nick);
|
||||
if nick.eq_ignore_ascii_case(account) {
|
||||
ctx.notice(me, from.uid, "You can't ungroup your main account name.");
|
||||
return;
|
||||
}
|
||||
if db.resolve_account(nick).map_or(true, |a| !a.eq_ignore_ascii_case(account)) {
|
||||
ctx.notice(me, from.uid, format!("\x02{nick}\x02 isn't grouped to your account."));
|
||||
return;
|
||||
}
|
||||
match db.ungroup_nick(nick) {
|
||||
Ok(true) => ctx.notice(me, from.uid, format!("\x02{nick}\x02 is no longer grouped to \x02{account}\x02.")),
|
||||
Ok(false) => ctx.notice(me, from.uid, format!("\x02{nick}\x02 isn't a grouped nick.")),
|
||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue