use std::collections::HashMap;
use std::path::PathBuf;
use matrix_sdk::event_handler::Ctx;
use matrix_sdk::ruma::events::call::answer::CallAnswerEvent;
use matrix_sdk::ruma::events::poll::unstable_start::{
UnstablePollStartEvent,
UnstablePollStartEventContent,
};
use matrix_sdk::ruma::events::poll::unstable_end::UnstablePollEndEvent;
use matrix_sdk::ruma::events::poll::unstable_response::UnstablePollResponseEvent;
use matrix_sdk::ruma::events::reaction::ReactionEvent;
use matrix_sdk::ruma::events::room::aliases::RoomAliasesEvent;
use matrix_sdk::ruma::events::room::encrypted::RoomEncryptedEvent;
use matrix_sdk::ruma::events::room::member::{
RoomMemberEvent,
MembershipChange,
};
use matrix_sdk::ruma::events::room::message::{
MessageType,
RoomMessageEvent,
};
use matrix_sdk::ruma::events::room::name::RoomNameEvent;
use matrix_sdk::ruma::events::room::redaction::RoomRedactionEvent;
use matrix_sdk::ruma::events::room::tombstone::RoomTombstoneEvent;
use matrix_sdk::ruma::events::room::topic::RoomTopicEvent;
use matrix_sdk::ruma::events::sticker::StickerEvent;
use matrix_sdk::ruma::events::{
AnyMessageLikeEvent,
AnyStateEvent,
AnySyncTimelineEvent,
AnyTimelineEvent,
};
use matrix_sdk::ruma::RoomVersionId;
use matrix_sdk::{
Client,
Room,
};
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use tokio::sync::mpsc;
use crate::prelude::*;
#[derive(Debug, Deserialize)]
#[serde(default)]
#[doc(hidden)]
pub struct Config {
enabled: bool,
queue: usize,
}
impl Default for Config {
fn default() -> Self {
Self {
enabled: true,
queue: 100,
}
}
}
#[doc(hidden)]
pub fn load(bot: Rdzobot) {
if !bot.config().module.log.enabled {
return;
}
tracing::debug!("log:init()");
let (tx, rx) = mpsc::channel(std::cmp::max(bot.config().module.log.queue, 1));
tokio::spawn(log_writer(bot.state_dir().join("log"), rx));
bot.client.add_event_handler_context(LogSender(tx));
bot.add_event_handler(on_event);
}
async fn log_writer(
logdir: PathBuf,
mut rx: mpsc::Receiver<AnyTimelineEvent>,
) -> anyhow::Result<()> {
tokio::fs::create_dir_all(&logdir).await?;
let mut files: HashMap<String, File> = HashMap::new();
while let Some(event) = rx.recv().await {
let room_id = event.room_id().as_str().to_string();
let file: &mut tokio::fs::File = match files.get_mut(&room_id) {
Some(file) => file,
None => {
let path = logdir.join(event.room_id().as_str());
match File::options().append(true).create(true).open(&path).await {
Ok(file) => {
files.insert(room_id.clone(), file);
files.get_mut(&room_id).unwrap()
}
Err(e) => {
tracing::error!("can't open logfile for {room_id} ({}): {e}", path.display());
continue
}
}
}
};
file.write_all(format_event(&event).as_bytes()).await?;
}
Ok(())
}
#[derive(Clone)]
struct LogSender(mpsc::Sender<AnyTimelineEvent>);
async fn on_event(
event: AnySyncTimelineEvent,
_client: Client,
room: Room,
tx: Ctx<LogSender>
) -> anyhow::Result<()> {
Ok(tx.0.0.send(event.into_full_event(room.room_id().into())).await?)
}
fn format_event(event: &AnyTimelineEvent) -> String {
let e_id = event.event_id();
let e_ts = format!("{:?}", &event.origin_server_ts());
let e_se = event.sender();
match event {
AnyTimelineEvent::MessageLike(event) => match event {
AnyMessageLikeEvent::Reaction(event) => match event {
ReactionEvent::Original(event) => format!(
"{} {} * {} reacted {:?} to {}\n",
e_id, e_ts, e_se,
event.content.relates_to.key,
event.content.relates_to.event_id,
),
ReactionEvent::Redacted(event) => format!(
"{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event,
),
}
AnyMessageLikeEvent::RoomMessage(event) => match event {
RoomMessageEvent::Original(event) => match &event.content.msgtype {
MessageType::Audio(_content) => format!(
"{} {} <{}> [audio]\n", e_id, e_ts, e_se,
),
MessageType::Emote(content) => format!(
"{} {} <{}> [emote: {}]\n",
e_id, e_ts, e_se, content.body,
),
MessageType::File(content) => {
if let Some(filename) = &content.filename {
format!("{} {} <{}> [file: {} description: {}]\n",
e_id, e_ts, e_se, filename, content.body,
)
} else {
format!("{} {} <{}> [file: {}]\n",
e_id, e_ts, e_se, content.body,
)
}
}
MessageType::Image(content) => {
if let Some(filename) = &content.filename {
format!("{} {} <{}> [image: {} description: {}]\n",
e_id, e_ts, e_se, filename, content.body,
)
} else {
format!("{} {} <{}> [image: {}]\n",
e_id, e_ts, e_se, content.body,
)
}
}
MessageType::Location(content) => format!(
"{} {} <{}> [location: {}]\n",
e_id, e_ts, e_se, content.geo_uri,
),
MessageType::Notice(content) => format!(
"{} {} -{}- {}\n",
e_id, e_ts, e_se, content.body,
),
MessageType::ServerNotice(content) => format!(
"{} {} *{}* SERVER NOTICE ({}): {}\n",
e_id, e_ts,
e_se,
content.server_notice_type.as_str(),
content.body,
),
MessageType::Text(content) => format!(
"{} {} <{}> {}\n",
e_id, e_ts, e_se, content.body,
),
MessageType::Video(content) => {
if let Some(filename) = &content.filename {
format!("{} {} <{}> [video: {} description: {}]\n",
e_id, e_ts, e_se, filename, content.body,
)
} else {
format!("{} {} <{}> [video: {}]\n",
e_id, e_ts, e_se, content.body,
)
}
}
MessageType::VerificationRequest(content) => format!(
"{} {} <{}> [key verification request: {}]\n",
e_id, e_ts, e_se, content.body,
),
_ => format!(
"{} {} <{}> [unknown event: {:?}]\n",
e_id, e_ts, e_se, event,
),
},
RoomMessageEvent::Redacted(_) => format!(
"{} {} -{}- TODO {:?}\n",
e_id, e_ts, e_se, event,
),
},
AnyMessageLikeEvent::UnstablePollStart(event) => match event {
UnstablePollStartEvent::Original(event) => match &event.content {
UnstablePollStartEventContent::New(content) => format!(
"{} {} -!- {} started poll question: {} answers: {}\n",
e_id, e_ts, e_se,
content.poll_start.question.text,
content
.poll_start
.answers
.iter()
.map(|i| format!("[{}] {}", i.id, i.text))
.collect::<Vec<_>>()
.join(", "),
),
UnstablePollStartEventContent::Replacement(content) => match &content.poll_start {
Some(poll_start) => format!(
"{} {} -!- {} amended poll {} question: {} answers: {}\n",
e_id, e_ts, e_se, content.relates_to.event_id,
poll_start.question.text,
poll_start
.answers
.iter()
.map(|i| format!("[{}] {}", i.id, i.text))
.collect::<Vec<_>>()
.join(", "),
),
None => format!(
"{} {} -!- {} cleared poll {}\n",
e_id, e_ts, e_se, content.relates_to.event_id,
),
}
_ => format!(
"{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event,
),
}
UnstablePollStartEvent::Redacted(event) => format!(
"{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event,
),
}
AnyMessageLikeEvent::UnstablePollResponse(event) => match event {
UnstablePollResponseEvent::Original(event) => format!(
"{} {} -!- {} answered to poll {} questions {}\n",
e_id, e_ts, e_se,
event.content.relates_to.event_id,
event.content.poll_response.answers.join(", "),
),
UnstablePollResponseEvent::Redacted(event) => format!(
"{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event,
),
}
AnyMessageLikeEvent::UnstablePollEnd(event) => match event {
UnstablePollEndEvent::Original(event) => format!(
"{} {} -!- {} ended poll {:?}\n",
e_id, e_ts, e_se,
event.content.relates_to.event_id,
),
UnstablePollEndEvent::Redacted(event) => format!(
"{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event,
),
}
AnyMessageLikeEvent::CallAnswer(event) => match event {
CallAnswerEvent::Original(event) => format!(
"{} {} -!- {} answered call: {}\n",
e_id, e_ts, e_se, event.content.call_id.as_str(),
),
CallAnswerEvent::Redacted(event) => format!(
"{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event,
),
}
AnyMessageLikeEvent::CallInvite(_)
| AnyMessageLikeEvent::CallHangup(_)
| AnyMessageLikeEvent::CallCandidates(_)
| AnyMessageLikeEvent::CallNegotiate(_)
| AnyMessageLikeEvent::CallNotify(_)
| AnyMessageLikeEvent::CallReject(_)
| AnyMessageLikeEvent::CallSdpStreamMetadataChanged(_)
| AnyMessageLikeEvent::CallSelectAnswer(_)
| AnyMessageLikeEvent::KeyVerificationReady(_)
| AnyMessageLikeEvent::KeyVerificationStart(_)
| AnyMessageLikeEvent::KeyVerificationCancel(_)
| AnyMessageLikeEvent::KeyVerificationAccept(_)
| AnyMessageLikeEvent::KeyVerificationKey(_)
| AnyMessageLikeEvent::KeyVerificationMac(_)
| AnyMessageLikeEvent::KeyVerificationDone(_) => format!(
"{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event,
),
AnyMessageLikeEvent::Location(_)
| AnyMessageLikeEvent::Message(_)
| AnyMessageLikeEvent::Beacon(_)
| AnyMessageLikeEvent::PollStart(_)
| AnyMessageLikeEvent::PollResponse(_)
| AnyMessageLikeEvent::PollEnd(_) => format!(
"{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event,
),
AnyMessageLikeEvent::RoomEncrypted(event) => match event {
RoomEncryptedEvent::Original(_) => format!(
"{} {} -!- {} encrypted the room\n",
e_id, e_ts, e_se,
),
RoomEncryptedEvent::Redacted(_) => format!(
"{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event,
),
}
AnyMessageLikeEvent::RoomRedaction(event) => match event {
RoomRedactionEvent::Original(event) => format!(
"{} {} -!- {} redacted {} ({})\n",
e_id, e_ts, e_se,
event.redacts(&RoomVersionId::V11),
if let Some(reason) = &event.content.reason {
format!("reason: {}", reason)
} else {
"no reason".to_string()
}
),
RoomRedactionEvent::Redacted(event) => format!(
"{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event,
),
}
AnyMessageLikeEvent::Sticker(event) => match event {
StickerEvent::Original(event) => format!(
"{} {} * {} sent a sticker {}\n",
e_id, e_ts, e_se,
event.content.body,
),
StickerEvent::Redacted(event) => format!(
"{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event,
),
}
_ => format!("{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event),
}
AnyTimelineEvent::State(event) => match event {
AnyStateEvent::PolicyRuleRoom(_)
| AnyStateEvent::PolicyRuleServer(_)
| AnyStateEvent::PolicyRuleUser(_)
| AnyStateEvent::RoomAvatar(_)
| AnyStateEvent::RoomCanonicalAlias(_)
| AnyStateEvent::RoomCreate(_)
| AnyStateEvent::RoomEncryption(_)
| AnyStateEvent::RoomGuestAccess(_)
| AnyStateEvent::RoomHistoryVisibility(_)
| AnyStateEvent::RoomJoinRules(_)
| AnyStateEvent::RoomPinnedEvents(_)
| AnyStateEvent::RoomPowerLevels(_)
| AnyStateEvent::RoomServerAcl(_)
| AnyStateEvent::RoomThirdPartyInvite(_)
| AnyStateEvent::SpaceChild(_)
| AnyStateEvent::SpaceParent(_)
| AnyStateEvent::BeaconInfo(_)
| AnyStateEvent::CallMember(_)
| AnyStateEvent::MemberHints(_)
=> format!("{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event),
AnyStateEvent::RoomAliases(event) => match event {
RoomAliasesEvent::Original(event) => format!(
"{} {} -{}- [new room aliases: {}]\n",
e_id, e_ts, e_se,
event.content.aliases.iter().map(|i| i.as_str()).collect::<Vec<_>>().join(", ")
),
RoomAliasesEvent::Redacted(event) => format!(
"{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event
),
}
AnyStateEvent::RoomMember(event) => match event {
RoomMemberEvent::Original(event) => {
match event.membership_change() {
MembershipChange::None => format!(
"{} {} -!- {} has made no change\n",
e_id, e_ts, event.state_key,
),
MembershipChange::Error => format!(
"{} {} -!- {} has made an error in membership\n",
e_id, e_ts, event.state_key,
),
MembershipChange::Joined => format!(
"{} {} -!- {} has joined\n",
e_id, e_ts, event.state_key,
),
MembershipChange::Left => format!(
"{} {} -!- {} has left\n",
e_id, e_ts, event.state_key,
),
MembershipChange::Banned => format!(
"{} {} -!- {} was banned by {}\n",
e_id, e_ts, event.state_key, e_se,
),
MembershipChange::Unbanned => format!(
"{} {} -!- {} was unbanned by {}\n",
e_id, e_ts, event.state_key, e_se,
),
MembershipChange::Kicked => format!(
"{} {} -!- {} was kicked by {}\n",
e_id, e_ts, event.state_key, e_se,
),
MembershipChange::Invited => format!(
"{} {} -!- {} was invited by {}\n",
e_id, e_ts, event.state_key, e_se,
),
MembershipChange::KickedAndBanned => format!(
"{} {} -!- {} was kicked and banned by {}\n",
e_id, e_ts, event.state_key, e_se,
),
MembershipChange::InvitationAccepted => format!(
"{} {} -!- {} accepted invitation\n",
e_id, e_ts, event.state_key,
),
MembershipChange::InvitationRejected => format!(
"{} {} -!- {} rejected invitation\n",
e_id, e_ts, event.state_key,
),
MembershipChange::InvitationRevoked => format!(
"{} {} -!- {}'s invitation was revoked by {}\n",
e_id, e_ts, event.state_key, e_se,
),
MembershipChange::Knocked => format!(
"{} {} -!- {} knocked\n",
e_id, e_ts, event.state_key,
),
MembershipChange::KnockAccepted => format!(
"{} {} -!- {}'s knock accepted by {}\n",
e_id, e_ts, event.state_key, e_se,
),
MembershipChange::KnockRetracted => format!(
"{} {} -!- {} retracted knock\n",
e_id, e_ts, event.state_key,
),
MembershipChange::KnockDenied => format!(
"{} {} -!- {}'s knock denied by {}\n",
e_id, e_ts, event.state_key, e_se,
),
MembershipChange::ProfileChanged {
displayname_change: _, avatar_url_change: _,
} => format!(
"{} {} -!- {} changed profile\n",
e_id, e_ts, event.state_key,
),
MembershipChange::NotImplemented => format!(
"{} {} -!- {} state change not implemented by matrix-rust-sdk: {:?}\n",
e_id, e_ts, event.state_key, event,
),
_ => format!("{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event),
}
}
RoomMemberEvent::Redacted(event) => format!(
"{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event
),
}
AnyStateEvent::RoomName(event) => match event {
RoomNameEvent::Original(event) => if !event.content.name.is_empty() {
format!(
"{} {} -!- {} changed room name to: {}\n",
e_id, e_ts, e_se, event.content.name,
)
} else {
format!(
"{} {} -!- {} removed room name\n",
e_id, e_ts, e_se,
)
}
RoomNameEvent::Redacted(event) => format!(
"{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event
),
}
AnyStateEvent::RoomTopic(event) => match event {
RoomTopicEvent::Original(event) => if !event.content.topic.is_empty() {
format!(
"{} {} -!- {} changed the topic to: {}\n",
e_id, e_ts, e_se, event.content.topic,
)
} else {
format!(
"{} {} -!- {} removed the topic\n",
e_id, e_ts, e_se,
)
},
RoomTopicEvent::Redacted(event) => format!(
"{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event
),
}
AnyStateEvent::RoomTombstone(event) => match event {
RoomTombstoneEvent::Original(event) => format!(
"{} {} † {} closed the channel (replacement: {}): {}\n",
e_id, e_ts, e_se,
event.content.replacement_room.as_str(),
event.content.body,
),
RoomTombstoneEvent::Redacted(event) => format!(
"{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event
),
}
_ => format!("{} {} -{}- TODO {:?}\n", e_id, e_ts, e_se, event),
}
}
}