use std::collections::HashSet;
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
use crate::{api::*, Client, Event};
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct Subscribes {
pub dids: Option<Vec<String>>,
pub handles: Option<Vec<String>>,
}
impl Subscribes {
pub fn is_match(&self, repo: &str) -> bool {
if let Some(dids) = &self.dids {
if dids.iter().any(|d| repo == *d) {
return true;
}
}
false
}
pub fn subscribe_repo<T: ToString>(&mut self, did: T) -> Result<()> {
match self.dids.as_mut() {
Some(dids) => {
dids.push(did.to_string());
}
None => {
self.dids = Some(vec![did.to_string()]);
}
}
Ok(())
}
pub fn unsubscribe_repo<T: ToString>(&mut self, did: T) -> Result<()> {
let did = did.to_string();
match self.dids.as_ref() {
Some(dids) => {
self.dids = Some(dids.into_iter().filter(|d| **d != did).cloned().collect());
}
None => bail!("no such did"),
}
Ok(())
}
pub fn subscribe_handle<T: ToString>(&mut self, handle: T) -> Result<()> {
match self.handles.as_mut() {
Some(handles) => {
let handle = handle.to_string();
if handles.iter().all(|h| *h != handle) {
handles.push(handle.to_string());
}
}
None => {
self.handles = Some(vec![handle.to_string()]);
}
}
Ok(())
}
pub fn unsubscribe_handle<T: ToString>(&mut self, handle: T) -> Result<()> {
let handle = handle.to_string();
match self.handles.as_ref() {
Some(handles) => {
self.handles = Some(
handles
.into_iter()
.filter(|h| **h != handle)
.cloned()
.collect(),
);
}
None => bail!("no such handle"),
}
Ok(())
}
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct Keywords {
pub includes: Option<Vec<String>>,
pub excludes: Option<Vec<String>>,
}
impl Keywords {
pub fn includes(&self, posts: &[AppBskyFeedPost]) -> bool {
let Some(includes) = &self.includes else {
return false;
};
posts
.iter()
.any(|p| includes.iter().any(|i| p.text.contains(i)))
}
pub fn excludes(&self, posts: &[AppBskyFeedPost]) -> bool {
let Some(excludes) = &self.excludes else {
return false;
};
posts
.iter()
.any(|p| excludes.iter().any(|i| p.text.contains(i)))
}
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct Langs {
pub includes: Option<Vec<String>>,
pub excludes: Option<Vec<String>>,
}
impl Langs {
pub fn includes(&self, posts: &[AppBskyFeedPost]) -> bool {
let Some(includes) = &self.includes else {
return false;
};
posts.iter().any(|p| {
includes
.iter()
.any(|i| p.langs.as_ref().map(|l| l.contains(i)).unwrap_or(false))
})
}
pub fn excludes(&self, posts: &[AppBskyFeedPost]) -> bool {
let Some(excludes) = &self.excludes else {
return false;
};
posts.iter().any(|p| {
excludes
.iter()
.any(|i| p.langs.as_ref().map(|l| l.contains(i)).unwrap_or(false))
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Filter {
pub name: String,
pub subscribes: Option<Subscribes>,
pub keywords: Option<Keywords>,
pub langs: Option<Langs>,
}
impl Default for Filter {
fn default() -> Self {
Self {
name: String::new(),
subscribes: Some(Subscribes {
dids: Some(Vec::new()),
handles: Some(Vec::new()),
}),
keywords: Some(Keywords {
includes: Some(Vec::new()),
excludes: Some(Vec::new()),
}),
langs: Some(Langs {
includes: Some(Vec::new()),
excludes: Some(Vec::new()),
}),
}
}
}
impl Filter {
pub fn init(&mut self, client: &mut Client) {
let Some(follows) = self.subscribes.as_mut() else {
return;
};
let Some(handles) = &follows.handles else {
return;
};
let converted = handles
.iter()
.filter_map(|h| client.get_handle(h).ok())
.collect::<HashSet<_>>();
let dids = follows
.dids
.clone()
.map(|d| d.into_iter().collect::<HashSet<_>>())
.unwrap_or_default();
let dids = dids.union(&converted).cloned().collect::<Vec<_>>();
if dids.is_empty() {
follows.dids = None;
} else {
follows.dids = Some(dids);
}
log::debug!("{:?}", self);
}
fn is_follows_match(&self, commit: &ComAtprotoSyncSubscribereposCommit) -> bool {
match &self.subscribes {
Some(f) => f.is_match(&commit.repo),
None => false,
}
}
fn is_handle_match(&self, handle: &ComAtprotoSyncSubscribereposHandle) -> bool {
match &self.subscribes {
Some(f) => f.is_match(&handle.did),
None => false,
}
}
pub fn is_match(&self, event: &Event) -> bool {
if self.subscribes.is_none() && self.keywords.is_none() && self.langs.is_none() {
return true;
}
match &event.payload {
ComAtprotoSyncSubscribereposMainMessage::ComAtprotoSyncSubscribereposCommit(c) => {
let posts = c
.get_post()
.into_iter()
.map(|(_, fp)| fp)
.collect::<Vec<_>>();
match self.is_follows_match(c) {
true => {
!self
.keywords
.as_ref()
.map(|k| k.excludes(&posts))
.unwrap_or(false)
&& !self
.langs
.as_ref()
.map(|l| l.excludes(&posts))
.unwrap_or(false)
}
false => {
self
.keywords
.as_ref()
.map(|k| k.includes(&posts))
.unwrap_or(false)
|| self
.langs
.as_ref()
.map(|l| l.includes(&posts))
.unwrap_or(false)
}
}
}
ComAtprotoSyncSubscribereposMainMessage::ComAtprotoSyncSubscribereposHandle(h) => {
self.is_handle_match(h)
}
_ => true,
}
}
pub fn subscribe_repo<T: ToString>(&mut self, did: T) -> Result<()> {
if self.subscribes.is_none() {
self.subscribes = Some(Subscribes::default());
}
match self.subscribes.as_mut() {
Some(s) => s.subscribe_repo(did),
None => bail!("cannot modify filter"),
}
}
pub fn unsubscribe_repo<T: ToString>(&mut self, did: T) -> Result<()> {
if self.subscribes.is_none() {
bail!("no such did");
}
match self.subscribes.as_mut() {
Some(s) => s.unsubscribe_repo(did),
None => bail!("cannot modify filter"),
}
}
pub fn subscribe_handle<T: ToString>(&mut self, handle: T) -> Result<()> {
if self.subscribes.is_none() {
self.subscribes = Some(Subscribes::default());
}
match self.subscribes.as_mut() {
Some(s) => s.subscribe_handle(handle),
None => bail!("cannot modify filter"),
}
}
pub fn unsubscribe_handle<T: ToString>(&mut self, handle: T) -> Result<()> {
if self.subscribes.is_none() {
bail!("no such handle");
}
match self.subscribes.as_mut() {
Some(s) => s.unsubscribe_handle(handle),
None => bail!("cannot modify filter"),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Filters {
pub filters: Vec<Filter>,
}
impl Default for Filters {
fn default() -> Self {
Self {
filters: vec![Filter {
name: String::from("All"),
subscribes: None,
keywords: None,
langs: None,
}],
}
}
}
impl Filters {
pub fn init(&mut self, client: &mut Client) {
for filter in self.filters.iter_mut() {
filter.init(client);
}
}
pub fn add_timeline<T: ToString>(&self, client: &crate::api::Client, handle: T) -> Result<Self> {
let follows = client.app_bsky_graph_getfollows(&handle.to_string(), None, None)?;
let mut filters = self
.filters
.clone()
.into_iter()
.filter(|f| f.name != handle.to_string())
.collect::<Vec<_>>();
filters.push(Filter {
name: handle.to_string(),
subscribes: Some(Subscribes {
dids: Some(
follows
.follows
.iter()
.map(|f| f.did.clone())
.collect::<Vec<_>>(),
),
handles: None,
}),
..Default::default()
});
Ok(Self { filters })
}
pub fn remove_timeline<T: ToString>(&mut self, handle: T) {
self.filters = self
.filters
.clone()
.into_iter()
.filter(|f| f.name != handle.to_string())
.collect::<Vec<_>>();
}
pub fn get_filters(&self) -> Vec<Filter> {
self.filters.clone()
}
pub fn subscribe_repo<T1: ToString, T2: ToString>(&mut self, name: T1, did: T2) -> Result<()> {
let Some(filter) = self.filters.iter_mut().find(|f| f.name == name.to_string()) else {
bail!("no such named filter");
};
filter.subscribe_repo(did)
}
pub fn unsubscribe_repo<T1: ToString, T2: ToString>(&mut self, name: T1, did: T2) -> Result<()> {
let Some(filter) = self.filters.iter_mut().find(|f| f.name == name.to_string()) else {
bail!("no such named filter");
};
filter.unsubscribe_repo(did)
}
pub fn subscribe_handle<T1: ToString, T2: ToString>(
&mut self,
name: T1,
handle: T2,
) -> Result<()> {
let Some(filter) = self.filters.iter_mut().find(|f| f.name == name.to_string()) else {
bail!("no such named filter");
};
filter.subscribe_handle(handle)
}
pub fn unsubscribe_handle<T1: ToString, T2: ToString>(
&mut self,
name: T1,
did: T2,
) -> Result<()> {
let Some(filter) = self.filters.iter_mut().find(|f| f.name == name.to_string()) else {
bail!("no such named filter");
};
filter.unsubscribe_handle(did)
}
}