Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make history backend modular #126

Merged
merged 5 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
[workspace]
resolver = "2"
members = [
"sable_macros",
"sable_network",
"sable_ircd",
"client_listener",
"auth_client",
"client_listener",
"sable_ircd",
"sable_ipc",
"sable_history",
"sable_macros",
"sable_network",
"sable_server",
"sable_services",
]
Expand Down
12 changes: 12 additions & 0 deletions sable_history/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "sable_history"
version = "0.1.0"
edition = "2021"
build = "build.rs"

[build-dependencies]
built = { version = "0.5", features = [ "git2" ] }

[dependencies]
sable_macros = { path = "../sable_macros" }
sable_network = { path = "../sable_network" }
3 changes: 3 additions & 0 deletions sable_history/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
built::write_built_file().expect("Failed to acquire build-time information");
}
99 changes: 99 additions & 0 deletions sable_history/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//! History storage and retrieval

use std::collections::HashMap;

use sable_network::history::HistoryLogEntry;
use sable_network::network::state::{HistoricMessageSourceId, HistoricMessageTargetId};
use sable_network::prelude::*;

#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum TargetId {
User(UserId),
Channel(ChannelId),
}

impl From<UserId> for TargetId {
fn from(value: UserId) -> Self {
TargetId::User(value)
}
}

impl From<ChannelId> for TargetId {
fn from(value: ChannelId) -> Self {
TargetId::Channel(value)
}
}

impl TryFrom<&HistoricMessageSourceId> for TargetId {
type Error = ();

fn try_from(value: &HistoricMessageSourceId) -> Result<Self, Self::Error> {
match value {
HistoricMessageSourceId::Server(_) => Err(()), // Is that okay?
HistoricMessageSourceId::User(user) => Ok(TargetId::User(*user.user())),
HistoricMessageSourceId::Unknown => Err(()),
}
}
}
impl TryFrom<&HistoricMessageTargetId> for TargetId {
type Error = ();

fn try_from(value: &HistoricMessageTargetId) -> Result<Self, Self::Error> {
match value {
HistoricMessageTargetId::User(user) => Ok(TargetId::User(*user.user())),
HistoricMessageTargetId::Channel(channel) => Ok(TargetId::Channel(*channel)),
HistoricMessageTargetId::Unknown => Err(()),
}
}
}

pub enum HistoryRequest {
Latest {
to_ts: Option<i64>,
limit: usize,
},
Before {
from_ts: i64,
limit: usize,
},
After {
start_ts: i64,
limit: usize,
},
Around {
around_ts: i64,
limit: usize,
},
Between {
start_ts: i64,
end_ts: i64,
limit: usize,
},
}

/// A backend implementation of [IRCv3 CHATHISTORY](https://ircv3.net/specs/extensions/chathistory)
pub trait HistoryService {
/// Returns a list of list of history logs the given user has access to
///
/// And the timestamp of the last known message in that log.
fn list_targets(
&self,
user: UserId,
after_ts: Option<i64>,
before_ts: Option<i64>,
limit: Option<usize>,
) -> HashMap<TargetId, i64>;

fn get_entries(
&self,
user: UserId,
target: TargetId,
request: HistoryRequest,
) -> impl Iterator<Item = &HistoryLogEntry>;
}

pub mod local_history;

mod build_data {
include!(concat!(env!("OUT_DIR"), "/built.rs"));
}
198 changes: 198 additions & 0 deletions sable_history/src/local_history.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
use std::collections::HashMap;

use sable_network::network::state::HistoricMessageTargetId;
use sable_network::prelude::*;

use crate::*;

/// Helper to extract the target name for chathistory purposes from a given event.
///
/// This might be the source or target of the actual event, or might be None if it's
/// an event type that we don't include in history playback
fn target_id_for_entry(for_user: UserId, entry: &HistoryLogEntry) -> Option<TargetId> {
match &entry.details {
NetworkStateChange::NewMessage(message) => match &message.target {
HistoricMessageTargetId::User(user) if user.user() == &for_user => {
(&message.source).try_into().ok()
}
_ => (&message.target).try_into().ok(),
},
_ => None,
}
}

/// Implementation of [`HistoryService`] backed by [`NetworkNode`]
impl HistoryService for NetworkHistoryLog {
fn list_targets(
&self,
user: UserId,
after_ts: Option<i64>,
before_ts: Option<i64>,
limit: Option<usize>,
) -> HashMap<TargetId, i64> {
let mut found_targets = HashMap::new();

for entry in self.entries_for_user_reverse(user) {
if matches!(after_ts, Some(ts) if entry.timestamp >= ts) {
// Skip over until we hit the timestamp window we're interested in
continue;
}
if matches!(before_ts, Some(ts) if entry.timestamp <= ts) {
// We're iterating backwards through time; if we hit this then we've
// passed the requested window and should stop
break;
}

if let Some(target_id) = target_id_for_entry(user, entry) {
found_targets.entry(target_id).or_insert(entry.timestamp);
}

// If this pushes us past the the requested limit, stop
if matches!(limit, Some(limit) if limit <= found_targets.len()) {
break;
}
}

found_targets
}

fn get_entries(
&self,
user: UserId,
target: TargetId,
request: HistoryRequest,
) -> impl Iterator<Item = &HistoryLogEntry> {
match request {
#[rustfmt::skip]
HistoryRequest::Latest { to_ts, limit } => get_history_for_target(
self,
user,
target,
None,
to_ts,
limit,
0, // Forward limit
),

HistoryRequest::Before { from_ts, limit } => {
get_history_for_target(
self,
user,
target,
Some(from_ts),
None,
limit,
0, // Forward limit
)
}
HistoryRequest::After { start_ts, limit } => get_history_for_target(
self,
user,
target,
Some(start_ts),
None,
0, // Backward limit
limit,
),
HistoryRequest::Around { around_ts, limit } => {
get_history_for_target(
self,
user,
target,
Some(around_ts),
None,
limit / 2, // Backward limit
limit / 2, // Forward limit
)
}
HistoryRequest::Between {
start_ts,
end_ts,
limit,
} => get_history_for_target(
self,
user,
target,
Some(start_ts),
Some(end_ts),
0, // Backward limit
limit,
),
}
}
}

fn get_history_for_target(
log: &NetworkHistoryLog,
source: UserId,
target: TargetId,
from_ts: Option<i64>,
to_ts: Option<i64>,
backward_limit: usize,
forward_limit: usize,
) -> impl Iterator<Item = &HistoryLogEntry> {
let mut backward_entries = Vec::new();
let mut forward_entries = Vec::new();

if backward_limit != 0 {
let from_ts = if forward_limit == 0 {
from_ts
} else {
// HACK: This is AROUND so we want to capture messages whose timestamp matches exactly
// (it's a message in the middle of the range)
from_ts.map(|from_ts| from_ts + 1)
};

for entry in log.entries_for_user_reverse(source) {
if matches!(from_ts, Some(ts) if entry.timestamp >= ts) {
// Skip over until we hit the timestamp window we're interested in
continue;
}
if matches!(to_ts, Some(ts) if entry.timestamp <= ts) {
// If we hit this then we've passed the requested window and should stop
break;
}

if let Some(event_target) = target_id_for_entry(source, entry) {
if event_target == target {
backward_entries.push(entry);
}
}

if backward_limit <= backward_entries.len() {
break;
}
}
}

if forward_limit != 0 {
for entry in log.entries_for_user(source) {
if matches!(from_ts, Some(ts) if entry.timestamp <= ts) {
// Skip over until we hit the timestamp window we're interested in
continue;
}
if matches!(to_ts, Some(ts) if entry.timestamp >= ts) {
// If we hit this then we've passed the requested window and should stop
break;
}

if let Some(event_target) = target_id_for_entry(source, entry) {
if event_target == target {
forward_entries.push(entry);
}
}

if forward_limit <= forward_entries.len() {
break;
}
}
}

// "The order of returned messages within the batch is implementation-defined, but SHOULD be
// ascending time order or some approximation thereof, regardless of the subcommand used."
// -- https://ircv3.net/specs/extensions/chathistory#returned-message-notes
backward_entries
.into_iter()
.rev()
.chain(forward_entries.into_iter())
}
1 change: 1 addition & 0 deletions sable_ircd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ debug = []
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
sable_history = { path = "../sable_history" }
sable_macros = { path = "../sable_macros" }
sable_network = { path = "../sable_network" }
sable_server = { path = "../sable_server" }
Expand Down
Loading
Loading