use crate::prelude::*;
use crate::notbottime::NotBotTime;
fn default_due_keywords() -> Vec<String> {
vec!["due".s()]
}
fn default_due_me_keywords() -> Vec<String> {
vec!["due-me".s(), "dueme".s()]
}
#[derive(Clone, Deserialize)]
pub struct ModuleConfig {
pub nag_channels: Vec<String>,
pub nag_late_fees: i64,
pub due_others_allowed: Vec<String>,
#[serde(default = "default_due_keywords")]
pub keywords_due: Vec<String>,
#[serde(default = "default_due_me_keywords")]
pub keywords_due_me: Vec<String>,
pub capacifier_token: String,
}
pub(crate) fn starter(_: &Client, config: &Config) -> anyhow::Result<Vec<ModuleInfo>> {
info!("registering modules");
let module_config: ModuleConfig = config.typed_module_config(module_path!())?;
let (duetx, duerx) = mpsc::channel::<ConsumerEvent>(1);
let due = ModuleInfo {
name: "due".s(),
help: "checks how many membership fees a member is missing".s(),
acl: vec![Acl::Room(module_config.due_others_allowed.clone())],
trigger: TriggerType::Keyword(module_config.keywords_due.clone()),
channel: duetx,
error_prefix: Some("error checking membership fees".s()),
};
due.spawn(duerx, module_config.clone(), due_processor);
let (due_metx, due_merx) = mpsc::channel::<ConsumerEvent>(1);
let due_me = ModuleInfo {
name: "due-me".s(),
help: "checks how many membership fees you are missing".s(),
acl: vec![],
trigger: TriggerType::Keyword(module_config.keywords_due_me.clone()),
channel: due_metx,
error_prefix: Some("error checking membership fees".s()),
};
due_me.spawn(due_merx, module_config, due_me_processor);
Ok(vec![due, due_me])
}
pub async fn due_processor(event: ConsumerEvent, c: ModuleConfig) -> anyhow::Result<()> {
use MembershipStatus::{Active, Inactive, NotAMember, Stoned};
let Some(arguments) = event.args else {
bail!("missing argument: member");
};
let target = {
let mut candidate: Option<OwnedUserId> = None;
if let Some(mut mentions_set) = event.ev.content.mentions {
trace!("mentions: {mentions_set:#?}");
if let Some(userid) = mentions_set.user_ids.pop_first() {
candidate = Some(userid);
}
}
if candidate.is_none() {
let mut args = arguments.split_whitespace();
if let Some(plain_target) = args.next() {
let maybe_mxid = format!("@{plain_target}:hackerspace.pl");
candidate = UserId::parse(maybe_mxid).ok();
};
};
let Some(found) = candidate else {
bail!("member argument missing or we couldn't parse it");
};
found
};
let member = target.localpart();
let response = match membership_status(c.capacifier_token, target.clone()).await? {
NotAMember => "not a member".s(),
Stoned => "stoned".s(),
Inactive => "not currently a member".s(),
Active(months) => match months {
i64::MIN..0 => format!("{member} is {} months ahead. Cool!", 0 - months),
0 => format!("{member} has paid all their membership fees."),
1 => format!("{member} needs to pay one membership fee."),
2..=i64::MAX => format!("{member} needs to pay {months} membership fees."),
},
};
event
.room
.send(RoomMessageEventContent::text_plain(response))
.await?;
Ok(())
}
pub async fn due_me_processor(event: ConsumerEvent, c: ModuleConfig) -> anyhow::Result<()> {
use MembershipStatus::{Active, Inactive, NotAMember, Stoned};
let response = match membership_status(c.capacifier_token, event.sender).await? {
NotAMember => "not a member".s(),
Stoned => "stoned".s(),
Inactive => "not currently a member".s(),
Active(months) => match months {
i64::MIN..0 => format!("{} months ahead. Cool!", 0 - months),
0 => "paid all membership fees.".s(),
1 => "need to pay one membership fee.".s(),
2..=i64::MAX => format!("need to pay {months} membership fees."),
},
};
event
.room
.send(RoomMessageEventContent::text_plain(response))
.await?;
Ok(())
}
pub(crate) fn passthrough(
_: &Client,
config: &Config,
) -> anyhow::Result<Vec<PassThroughModuleInfo>> {
info!("registering passthrough modules");
let module_config: ModuleConfig = config.typed_module_config(module_path!())?;
let (nagtx, nagrx) = mpsc::channel::<ConsumerEvent>(1);
let nag = PassThroughModuleInfo(ModuleInfo {
name: "nag".s(),
help: "nags users about missing membership fees".s(),
acl: vec![Acl::Room(module_config.nag_channels.clone())],
trigger: TriggerType::Catchall(|_, _, _, _, _| Ok(Consumption::Inclusive)),
channel: nagtx,
error_prefix: None,
});
nag.0.spawn(nagrx, module_config, nag_processor);
Ok(vec![nag])
}
pub async fn nag_processor(event: ConsumerEvent, config: ModuleConfig) -> anyhow::Result<()> {
use MembershipStatus::Active;
let sender_str: &str = event.sender.as_str();
trace!("getting client store");
let c = event.room.client();
let store = c.state_store();
let next_nag_time = match store.get_custom_value(event.sender.as_bytes()).await {
Ok(maybe_result) => match maybe_result {
None => {
let nag_time = NotBotTime(SystemTime::now() - Duration::new(60 * 60 * 24, 0));
let nag_time_bytes: Vec<u8> = nag_time.into();
store
.set_custom_value_no_read(event.sender.as_bytes(), nag_time_bytes)
.await?;
nag_time
}
Some(nag_time_bytes) => nag_time_bytes.into(),
},
Err(e) => bail!("error fetching nag time: {e}"),
};
trace!("next_nag_time: {:#?}", next_nag_time);
if NotBotTime::now() > next_nag_time {
let next_nag_time = NotBotTime(SystemTime::now() + Duration::new(60 * 60 * 24, 0));
store
.set_custom_value_no_read(event.sender.as_bytes(), next_nag_time.into())
.await?;
} else {
return Ok(());
};
let Ok(Active(months)) = membership_status(config.capacifier_token, event.sender.clone()).await
else {
return Ok(());
};
if months < config.nag_late_fees {
debug!("too early to nag: {months}");
return Ok(());
};
let period = match months {
i64::MIN..=0 => {
return Ok(());
}
1 => "month",
_ => "months",
};
let member_display_name: String = match event.room.get_member(&event.sender).await {
Ok(Some(rm)) => rm
.display_name()
.map_or_else(|| sender_str.to_owned(), std::borrow::ToOwned::to_owned),
_ => sender_str.to_owned(),
};
let msg_text = format!("pay your membership fees! you are {months} {period} behind!");
let plain_message = format!(r"{member_display_name}: {msg_text}");
let html_message = format!(
r#"<a href="{uri}">{display_name}</a>: {text}"#,
uri = event.sender.matrix_to_uri(),
display_name = member_display_name,
text = msg_text
);
let msg = RoomMessageEventContent::text_html(plain_message, html_message)
.add_mentions(Mentions::with_user_ids(vec![event.sender]));
event.room.send(msg).await?;
Ok(())
}