forked from MightyPork/group-actor
parent
1ea3aa7cb0
commit
5a631f785e
@ -0,0 +1,209 @@ |
||||
use std::sync::Arc; |
||||
use std::time::{Duration, Instant}; |
||||
|
||||
use elefren::{FediClient, StatusBuilder}; |
||||
use elefren::debug::EventDisplay; |
||||
use elefren::debug::NotificationDisplay; |
||||
use elefren::entities::event::Event; |
||||
use elefren::entities::notification::{Notification, NotificationType}; |
||||
use elefren::status_builder::Visibility; |
||||
use futures::StreamExt; |
||||
|
||||
use crate::store::{ConfigStore, GroupError}; |
||||
use crate::store::data::GroupConfig; |
||||
use crate::utils::LogError; |
||||
|
||||
/// This is one group's config store capable of persistence
|
||||
#[derive(Debug)] |
||||
pub struct GroupHandle { |
||||
pub(crate) client: FediClient, |
||||
pub(crate) config: GroupConfig, |
||||
pub(crate) store: Arc<ConfigStore>, |
||||
} |
||||
|
||||
impl GroupHandle { |
||||
pub async fn save(&mut self) -> Result<(), GroupError> { |
||||
debug!("Saving group config & status"); |
||||
self.store.set_group_config(self.config.clone()).await?; |
||||
self.config.clear_dirty_status(); |
||||
Ok(()) |
||||
} |
||||
|
||||
pub async fn save_if_needed(&mut self) -> Result<(), GroupError> { |
||||
if self.config.is_dirty() { |
||||
self.save().await?; |
||||
} |
||||
Ok(()) |
||||
} |
||||
|
||||
pub async fn reload(&mut self) -> Result<(), GroupError> { |
||||
if let Some(g) = self.store.get_group_config(self.config.get_acct()).await { |
||||
self.config = g; |
||||
Ok(()) |
||||
} else { |
||||
Err(GroupError::GroupNotExist) |
||||
} |
||||
} |
||||
} |
||||
|
||||
trait NotifTimestamp { |
||||
fn timestamp_millis(&self) -> u64; |
||||
} |
||||
|
||||
impl NotifTimestamp for Notification { |
||||
fn timestamp_millis(&self) -> u64 { |
||||
self.created_at.timestamp_millis().max(0) as u64 |
||||
} |
||||
} |
||||
|
||||
impl GroupHandle { |
||||
pub async fn run(&mut self) -> Result<(), GroupError> { |
||||
const PERIODIC_SAVE: Duration = Duration::from_secs(60); |
||||
const PING_INTERVAL: Duration = Duration::from_secs(15); |
||||
|
||||
assert!(PERIODIC_SAVE >= PING_INTERVAL); |
||||
|
||||
let mut next_save = Instant::now() + PERIODIC_SAVE; // so we save at start
|
||||
|
||||
loop { |
||||
debug!("Opening streaming API socket"); |
||||
let mut events = self.client.streaming_user().await?; |
||||
|
||||
match self.catch_up_with_missed_notifications().await { |
||||
Ok(true) => { |
||||
debug!("Some missed notifs handled"); |
||||
// Save asap!
|
||||
next_save = Instant::now() - PERIODIC_SAVE |
||||
} |
||||
Ok(false) => { |
||||
debug!("No notifs missed"); |
||||
} |
||||
Err(e) => { |
||||
error!("Failed to handle missed notifs: {}", e); |
||||
} |
||||
} |
||||
|
||||
loop { |
||||
if next_save < Instant::now() { |
||||
self.save_if_needed().await |
||||
.log_error("Failed to save group"); |
||||
next_save = Instant::now() + PERIODIC_SAVE; |
||||
} |
||||
|
||||
let timeout = next_save.saturating_duration_since(Instant::now()) |
||||
.min(PING_INTERVAL) |
||||
.max(Duration::from_secs(1)); |
||||
|
||||
match tokio::time::timeout(timeout, events.next()).await { |
||||
Ok(Some(event)) => { |
||||
debug!("(@{}) Event: {}", self.config.get_acct(), EventDisplay(&event)); |
||||
match event { |
||||
Event::Update(_status) => {} |
||||
Event::Notification(n) => { |
||||
self.handle_notification(n).await; |
||||
} |
||||
Event::Delete(_id) => {} |
||||
Event::FiltersChanged => {} |
||||
} |
||||
} |
||||
Ok(None) => { |
||||
warn!("Group @{} socket closed, restarting...", self.config.get_acct()); |
||||
break; |
||||
} |
||||
Err(_) => { |
||||
// Timeout so we can save if needed
|
||||
} |
||||
} |
||||
|
||||
trace!("Pinging"); |
||||
events.send_ping().await.log_error("Fail to send ping"); |
||||
} |
||||
|
||||
warn!("Notif stream closed, will reopen"); |
||||
tokio::time::sleep(Duration::from_millis(1000)).await; |
||||
} |
||||
} |
||||
|
||||
async fn handle_notification(&mut self, n: Notification) { |
||||
debug!("Handling notif #{}", n.id); |
||||
let ts = n.timestamp_millis(); |
||||
self.config.set_last_notif(ts); |
||||
|
||||
match n.notification_type { |
||||
NotificationType::Mention => { |
||||
if let Some(status) = n.status { |
||||
if status.content.contains("/gi") || status.content.contains("\\gi") { |
||||
info!("Mention ignored by gi"); |
||||
} else if status.content.contains("/gb") || status.content.contains("\\gb") { |
||||
if let Some(id) = status.in_reply_to_id { |
||||
info!("Boosting prev post by GB"); |
||||
tokio::time::sleep(Duration::from_millis(500)).await; |
||||
// self.client.reblog(&id).await.log_error("Failed to boost");
|
||||
} |
||||
} else { |
||||
info!("Boosting mention"); |
||||
tokio::time::sleep(Duration::from_millis(500)).await; |
||||
// self.client.reblog(&status.id).await.log_error("Failed to boost");
|
||||
} |
||||
} |
||||
} |
||||
NotificationType::Follow => { |
||||
info!("New follower!"); |
||||
tokio::time::sleep(Duration::from_millis(500)).await; |
||||
|
||||
/* |
||||
let post = StatusBuilder::new() |
||||
.status(format!("@{} welcome to the group!", &n.account.acct)) |
||||
.content_type("text/markdown") |
||||
.visibility(Visibility::Unlisted) |
||||
.build().expect("error build status"); |
||||
|
||||
let _ = self.client.new_status(post).await.log_error("Failed to post"); |
||||
*/ |
||||
} |
||||
_ => {} |
||||
} |
||||
} |
||||
|
||||
/// Catch up with missed notifications, returns true if any were handled
|
||||
async fn catch_up_with_missed_notifications(&mut self) -> Result<bool, GroupError> { |
||||
const MAX_CATCHUP_NOTIFS: usize = 25; |
||||
let last_notif = self.config.get_last_notif(); |
||||
|
||||
let notifications = self.client.notifications().await?; |
||||
let mut iter = notifications.items_iter(); |
||||
|
||||
let mut notifs_to_handle = vec![]; |
||||
|
||||
// They are retrieved newest first, but we want oldest first for chronological handling
|
||||
|
||||
let mut num = 0; |
||||
while let Some(n) = iter.next_item().await { |
||||
let ts = n.timestamp_millis(); |
||||
if ts <= last_notif { |
||||
break; // reached our last seen notif
|
||||
} |
||||
notifs_to_handle.push(n); |
||||
num += 1; |
||||
if num > MAX_CATCHUP_NOTIFS { |
||||
warn!("Too many notifs missed to catch up!"); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if notifs_to_handle.is_empty() { |
||||
return Ok(false); |
||||
} |
||||
|
||||
notifs_to_handle.reverse(); |
||||
|
||||
debug!("{} notifications to catch up!", notifs_to_handle.len()); |
||||
|
||||
for n in notifs_to_handle { |
||||
debug!("Handling missed notification: {}", NotificationDisplay(&n)); |
||||
self.handle_notification(n).await; |
||||
} |
||||
|
||||
return Ok(true); |
||||
} |
||||
} |
@ -0,0 +1,333 @@ |
||||
use std::collections::{HashMap, HashSet}; |
||||
|
||||
use crate::store; |
||||
use crate::store::GroupError; |
||||
use elefren::AppData; |
||||
|
||||
/// This is the inner data struct holding the config
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)] |
||||
pub(crate) struct Config { |
||||
groups: HashMap<String, GroupConfig>, |
||||
} |
||||
|
||||
impl Config { |
||||
pub(crate) fn iter_groups(&self) -> impl Iterator<Item=&GroupConfig>{ |
||||
self.groups.values() |
||||
} |
||||
|
||||
pub(crate) fn get_group_config(&self, acct : &str) -> Option<&GroupConfig> { |
||||
self.groups.get(acct) |
||||
} |
||||
|
||||
pub(crate) fn set_group_config(&mut self, grp : GroupConfig) { |
||||
self.groups.insert(grp.acct.clone(), grp); |
||||
} |
||||
|
||||
} |
||||
|
||||
/// This is the inner data struct holding a group's config
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)] |
||||
#[serde(default)] |
||||
pub(crate) struct GroupConfig { |
||||
enabled: bool, |
||||
/// Group actor's acct
|
||||
acct: String, |
||||
/// elefren data
|
||||
appdata : AppData, |
||||
/// List of admin account "acct" names, e.g. piggo@piggo.space
|
||||
admin_users: HashSet<String>, |
||||
/// List of users allowed to post to the group, if it is member-only
|
||||
member_users: HashSet<String>, |
||||
/// List of users banned from posting to the group
|
||||
banned_users: HashSet<String>, |
||||
/// True if only members should be allowed to write
|
||||
member_only: bool, |
||||
/// Banned domain names, e.g. kiwifarms.cc
|
||||
banned_servers: HashSet<String>, |
||||
/// Last seen notification timestamp
|
||||
last_notif_ts: u64, |
||||
|
||||
#[serde(skip)] |
||||
dirty: bool, |
||||
} |
||||
|
||||
impl Default for GroupConfig { |
||||
fn default() -> Self { |
||||
Self { |
||||
enabled: true, |
||||
acct: "".to_string(), |
||||
appdata: AppData { |
||||
base: Default::default(), |
||||
client_id: Default::default(), |
||||
client_secret: Default::default(), |
||||
redirect: Default::default(), |
||||
token: Default::default() |
||||
}, |
||||
admin_users: Default::default(), |
||||
member_users: Default::default(), |
||||
banned_users: Default::default(), |
||||
member_only: false, |
||||
banned_servers: Default::default(), |
||||
last_notif_ts: 0, |
||||
dirty: false, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl GroupConfig { |
||||
pub(crate) fn new(acct : String, appdata: AppData) -> Self { |
||||
Self { |
||||
acct, |
||||
appdata, |
||||
..Default::default() |
||||
} |
||||
} |
||||
|
||||
pub(crate) fn is_enabled(&self) -> bool { |
||||
self.enabled |
||||
} |
||||
|
||||
pub(crate) fn set_enabled(&mut self, ena: bool){ |
||||
self.enabled = ena; |
||||
self.mark_dirty(); |
||||
} |
||||
|
||||
pub(crate) fn get_appdata(&self) -> &AppData { |
||||
&self.appdata |
||||
} |
||||
|
||||
pub(crate) fn set_appdata(&mut self, appdata: AppData) { |
||||
self.appdata = appdata; |
||||
self.mark_dirty(); |
||||
} |
||||
|
||||
pub(crate) fn set_last_notif(&mut self, ts: u64) { |
||||
self.last_notif_ts = self.last_notif_ts.max(ts); |
||||
self.mark_dirty(); |
||||
} |
||||
|
||||
pub(crate) fn get_last_notif(&self) -> u64 { |
||||
self.last_notif_ts |
||||
} |
||||
|
||||
pub(crate) fn get_acct(&self) -> &str { |
||||
&self.acct |
||||
} |
||||
|
||||
pub(crate) fn is_admin(&self, acct: &str) -> bool { |
||||
self.admin_users.contains(acct) |
||||
} |
||||
|
||||
pub(crate) fn is_member(&self, acct: &str) -> bool { |
||||
self.member_users.contains(acct) |
||||
} |
||||
|
||||
pub(crate) fn is_banned(&self, acct: &str) -> bool { |
||||
self.banned_users.contains(acct) |
||||
|| self.is_users_server_banned(acct) |
||||
} |
||||
|
||||
/// Check if the user's server is banned
|
||||
fn is_users_server_banned(&self, acct: &str) -> bool { |
||||
let server = acct_to_server(acct); |
||||
self.banned_servers.contains(server) |
||||
} |
||||
|
||||
pub(crate) fn can_write(&self, acct: &str) -> bool { |
||||
if self.is_admin(acct) { |
||||
true |
||||
} else { |
||||
!self.is_banned(acct) && ( |
||||
!self.is_member_only() |
||||
|| self.is_member(acct) |
||||
) |
||||
} |
||||
} |
||||
|
||||
pub(crate) fn set_admin(&mut self, acct: &str, admin: bool) -> Result<(), GroupError> { |
||||
if admin { |
||||
if self.is_banned(acct) { |
||||
return Err(GroupError::UserIsBanned); |
||||
} |
||||
self.admin_users.insert(acct.to_owned()); |
||||
} else { |
||||
self.admin_users.remove(acct); |
||||
} |
||||
self.mark_dirty(); |
||||
Ok(()) |
||||
} |
||||
|
||||
pub(crate) fn set_member(&mut self, acct: &str, member: bool) -> Result<(), GroupError> { |
||||
if member { |
||||
if self.is_banned(acct) { |
||||
return Err(GroupError::UserIsBanned); |
||||
} |
||||
self.member_users.insert(acct.to_owned()); |
||||
} else { |
||||
self.member_users.remove(acct); |
||||
} |
||||
self.mark_dirty(); |
||||
Ok(()) |
||||
} |
||||
|
||||
pub(crate) fn ban_user(&mut self, acct: &str, ban: bool) -> Result<(), GroupError> { |
||||
if ban { |
||||
if self.is_admin(acct) { |
||||
return Err(GroupError::UserIsAdmin); |
||||
} |
||||
self.banned_users.insert(acct.to_owned()); |
||||
} else { |
||||
self.banned_users.remove(acct); |
||||
} |
||||
Ok(()) |
||||
} |
||||
|
||||
pub(crate) fn ban_server(&mut self, server: &str, ban: bool) -> Result<(), GroupError> { |
||||
if ban { |
||||
for acct in &self.admin_users { |
||||
let acct_server = acct_to_server(acct); |
||||
if acct_server == server { |
||||
return Err(GroupError::AdminsOnServer); |
||||
} |
||||
} |
||||
self.banned_servers.insert(server.to_owned()); |
||||
} else { |
||||
self.banned_servers.remove(server); |
||||
} |
||||
self.mark_dirty(); |
||||
Ok(()) |
||||
} |
||||
|
||||
pub(crate) fn set_member_only(&mut self, member_only: bool) { |
||||
self.member_only = member_only; |
||||
self.mark_dirty(); |
||||
} |
||||
|
||||
pub(crate) fn is_member_only(&self) -> bool { |
||||
self.member_only |
||||
} |
||||
|
||||
pub(crate) fn mark_dirty(&mut self) { |
||||
self.dirty = true; |
||||
} |
||||
|
||||
pub(crate) fn is_dirty(&self) -> bool { |
||||
self.dirty |
||||
} |
||||
|
||||
pub(crate) fn clear_dirty_status(&mut self) { |
||||
self.dirty = false; |
||||
} |
||||
} |
||||
|
||||
fn acct_to_server(acct: &str) -> &str { |
||||
acct.split('@').nth(1) |
||||
.unwrap_or_default() |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use crate::store::{GroupError}; |
||||
use crate::store::data::{GroupConfig, acct_to_server}; |
||||
|
||||
#[test] |
||||
fn test_acct_to_server() { |
||||
assert_eq!("pikachu.rocks", acct_to_server("raichu@pikachu.rocks")); |
||||
assert_eq!("pikachu.rocks", acct_to_server("m@pikachu.rocks")); |
||||
assert_eq!("", acct_to_server("what")); |
||||
} |
||||
|
||||
#[test] |
||||
fn test_default_rules() { |
||||
let mut group = GroupConfig::default(); |
||||
assert!(!group.is_member_only()); |
||||
assert!(!group.is_member("piggo@piggo.space")); |
||||
assert!(!group.is_admin("piggo@piggo.space")); |
||||
assert!(group.can_write("piggo@piggo.space"), "anyone can post by default"); |
||||
} |
||||
|
||||
#[test] |
||||
fn test_member_only() { |
||||
let mut group = GroupConfig::default(); |
||||
assert!(group.can_write("piggo@piggo.space"), "rando can write in public group"); |
||||
|
||||
group.set_member_only(true); |
||||
assert!(!group.can_write("piggo@piggo.space"), "rando can't write in member-only group"); |
||||
|
||||
// Admin in member only
|
||||
group.set_admin("piggo@piggo.space", true).unwrap(); |
||||
assert!(group.can_write("piggo@piggo.space"), "admin non-member can write in member-only group"); |
||||
group.set_admin("piggo@piggo.space", false).unwrap(); |
||||
assert!(!group.can_write("piggo@piggo.space"), "removed admin removes privileged write access"); |
||||
|
||||
// Member in member only
|
||||
group.set_member("piggo@piggo.space", true).unwrap(); |
||||
assert!(group.can_write("piggo@piggo.space"), "member can post in member-only group"); |
||||
group.set_admin("piggo@piggo.space", true).unwrap(); |
||||
assert!(group.can_write("piggo@piggo.space"), "member+admin can post in member-only group"); |
||||
} |
||||
|
||||
#[test] |
||||
fn test_banned_users() { |
||||
// Banning single user
|
||||
let mut group = GroupConfig::default(); |
||||
group.ban_user("piggo@piggo.space", true).unwrap(); |
||||
assert!(!group.can_write("piggo@piggo.space"), "banned user can't post"); |
||||
group.ban_user("piggo@piggo.space", false).unwrap(); |
||||
assert!(group.can_write("piggo@piggo.space"), "un-ban works"); |
||||
} |
||||
|
||||
#[test] |
||||
fn test_banned_members() { |
||||
// Banning single user
|
||||
let mut group = GroupConfig::default(); |
||||
group.set_member_only(true); |
||||
|
||||
group.set_member("piggo@piggo.space", true).unwrap(); |
||||
assert!(group.can_write("piggo@piggo.space"), "member can write"); |
||||
assert!(group.is_member("piggo@piggo.space"), "member is member"); |
||||
assert!(!group.is_banned("piggo@piggo.space"), "user not banned by default"); |
||||
|
||||
group.ban_user("piggo@piggo.space", true).unwrap(); |
||||
assert!(group.is_member("piggo@piggo.space"), "still member even if banned"); |
||||
assert!(group.is_banned("piggo@piggo.space"), "banned user is banned"); |
||||
|
||||
assert!(!group.can_write("piggo@piggo.space"), "banned member can't post"); |
||||
|
||||
// unban
|
||||
group.ban_user("piggo@piggo.space", false).unwrap(); |
||||
assert!(group.can_write("piggo@piggo.space"), "un-ban works"); |
||||
} |
||||
|
||||
#[test] |
||||
fn test_server_ban() { |
||||
let mut group = GroupConfig::default(); |
||||
assert!(group.can_write("hitler@nazi.camp"), "randos can write"); |
||||
|
||||
group.ban_server("nazi.camp", true).unwrap(); |
||||
assert!(!group.can_write("hitler@nazi.camp"), "users from banned server can't write"); |
||||
assert!(!group.can_write("1488@nazi.camp"), "users from banned server can't write"); |
||||
assert!(group.can_write("troll@freezepeach.xyz"), "other users can still write"); |
||||
|
||||
group.ban_server("nazi.camp", false).unwrap(); |
||||
assert!(group.can_write("hitler@nazi.camp"), "server unban works"); |
||||
} |
||||
|
||||
#[test] |
||||
fn test_sanity() { |
||||
let mut group = GroupConfig::default(); |
||||
|
||||
group.set_admin("piggo@piggo.space", true).unwrap(); |
||||
assert_eq!(Err(GroupError::UserIsAdmin), group.ban_user("piggo@piggo.space", true), "can't bad admin users"); |
||||
group.ban_user("piggo@piggo.space", false).expect("can unbad admin"); |
||||
|
||||
group.ban_user("hitler@nazi.camp", true).unwrap(); |
||||
assert_eq!(Err(GroupError::UserIsBanned), group.set_admin("hitler@nazi.camp", true), "can't make banned users admins"); |
||||
|
||||
group.ban_server("freespeechextremist.com", true).unwrap(); |
||||
assert_eq!(Err(GroupError::UserIsBanned), group.set_admin("nibber@freespeechextremist.com", true), "can't make server-banned users admins"); |
||||
|
||||
assert!(group.is_admin("piggo@piggo.space")); |
||||
assert_eq!(Err(GroupError::AdminsOnServer), group.ban_server("piggo.space", true), "can't bad server with admins"); |
||||
} |
||||
} |
@ -0,0 +1,225 @@ |
||||
use std::collections::{HashMap, HashSet}; |
||||
use std::hash::{Hash, Hasher}; |
||||
use std::path::{Path, PathBuf}; |
||||
use std::sync::Arc; |
||||
|
||||
use elefren::{FediClient, Registration, Scopes, scopes}; |
||||
use elefren::entities::event::Event; |
||||
use futures::StreamExt; |
||||
use tokio::sync::RwLock; |
||||
|
||||
use data::{Config, GroupConfig}; |
||||
|
||||
use crate::group_handle::GroupHandle; |
||||
|
||||
pub(crate) mod data; |
||||
|
||||
#[derive(Debug, Default)] |
||||
pub struct ConfigStore { |
||||
store_path: PathBuf, |
||||
save_pretty: bool, |
||||
data: tokio::sync::RwLock<Config>, |
||||
} |
||||
|
||||
#[derive(Debug)] |
||||
pub struct NewGroupOptions { |
||||
pub server: String, |
||||
pub acct: String, |
||||
} |
||||
|
||||
#[derive(Debug)] |
||||
pub struct StoreOptions { |
||||
pub store_path: String, |
||||
pub save_pretty: bool, |
||||
} |
||||
|
||||
impl ConfigStore { |
||||
/// Create a new instance of the store.
|
||||
/// If a path is given, it will try to load the content from a file.
|
||||
pub async fn new(options: StoreOptions) -> Result<Arc<Self>, GroupError> { |
||||
let path: &Path = options.store_path.as_ref(); |
||||
|
||||
let config = if path.is_file() { |
||||
let f = tokio::fs::read(path).await?; |
||||
serde_json::from_slice(&f)? |
||||
} else { |
||||
let empty = Config::default(); |
||||
tokio::fs::write(path, serde_json::to_string(&empty)?.as_bytes()).await?; |
||||
empty |
||||
}; |
||||
|
||||
Ok(Arc::new(Self { |
||||
store_path: path.to_owned(), |
||||
save_pretty: options.save_pretty, |
||||
data: RwLock::new(config), |
||||
})) |
||||
} |
||||
|
||||
/// Spawn a new group
|
||||
pub async fn auth_new_group(self: &Arc<Self>, opts: NewGroupOptions) -> Result<GroupHandle, GroupError> { |
||||
let registration = Registration::new(&opts.server) |
||||
.client_name("group-actor") |
||||
.force_login(true) |
||||
.scopes(make_scopes()) |
||||
.build().await?; |
||||
|
||||
println!("--- Authenticating NEW bot user @{} ---", opts.acct); |
||||
let client = elefren::helpers::cli::authenticate(registration).await?; |
||||
let appdata = client.data.clone(); |
||||
|
||||
let data = GroupConfig::new(opts.acct, appdata); |
||||
|
||||
// save & persist
|
||||
self.set_group_config(data.clone()).await?; |
||||
|
||||
Ok(GroupHandle { |
||||
client, |
||||
config: data, |
||||
store: self.clone(), |
||||
}) |
||||
} |
||||
|
||||
/// Re-auth an existing group
|
||||
pub async fn reauth_group(self: &Arc<Self>, acct: &str) -> Result<GroupHandle, GroupError> { |
||||
let groups = self.data.read().await; |
||||
let mut config = groups.get_group_config(acct).ok_or(GroupError::GroupNotExist)?.clone(); |
||||
|
||||
println!("--- Re-authenticating bot user @{} ---", acct); |
||||
let registration = Registration::new(config.get_appdata().base.to_string()) |
||||
.client_name("group-actor") |
||||
.force_login(true) |
||||
.scopes(make_scopes()) |
||||
.build().await?; |
||||
|
||||
let client = elefren::helpers::cli::authenticate(registration).await?; |
||||
let appdata = client.data.clone(); |
||||
|
||||
config.set_appdata(appdata); |
||||
self.set_group_config(config.clone()).await?; |
||||
|
||||
Ok(GroupHandle { |
||||
client, |
||||
config, |
||||
store: self.clone(), |
||||
}) |
||||
} |
||||
|
||||
/// Spawn existing group using saved creds
|
||||
pub async fn spawn_groups(self: Arc<Self>) -> Vec<GroupHandle> { |
||||
let groups = self.data.read().await; |
||||
let groups_iter = groups.iter_groups().cloned(); |
||||
|
||||
// Connect in parallel
|
||||
futures::stream::iter(groups_iter).map(|gc| async { |
||||
if !gc.is_enabled() { |
||||
debug!("Group @{} is DISABLED", gc.get_acct()); |
||||
return None; |
||||
} |
||||
|
||||
debug!("Connecting to @{}", gc.get_acct()); |
||||
|
||||
let client = FediClient::from(gc.get_appdata().clone()); |
||||
|
||||
match client.verify_credentials().await { |
||||
Ok(account) => { |
||||
info!("Group account verified: @{}, {}", account.acct, account.display_name); |
||||
} |
||||
Err(e) => { |
||||
error!("Group @{} auth error: {}", gc.get_acct(), e); |
||||
return None; |
||||
} |
||||
}; |
||||
|
||||
Some(GroupHandle { |
||||
client, |
||||
config: gc, |
||||
store: self.clone(), |
||||
}) |
||||
}).buffer_unordered(8).collect::<Vec<_>>().await |
||||
.into_iter().flatten().collect() |
||||
} |
||||
|
||||
pub(crate) async fn get_group_config(&self, group: &str) -> Option<GroupConfig> { |
||||
let c = self.data.read().await; |
||||
c.get_group_config(group).map(|inner| { |
||||
inner.clone() |
||||
}) |
||||
} |
||||
|
||||
//noinspection RsSelfConvention
|
||||
/// Set group config to the store. The store then saved.
|
||||
pub(crate) async fn set_group_config<'a>(&'a self, config: GroupConfig) -> Result<(), GroupError> { |
||||
let mut data = self.data.write().await; |
||||
data.set_group_config(config); |
||||
self.persist(&data).await?; |
||||
Ok(()) |
||||
} |
||||
|
||||
/// Persist the store
|
||||
async fn persist(&self, data: &Config) -> Result<(), GroupError> { |
||||
tokio::fs::write(&self.store_path, |
||||
if self.save_pretty { |
||||
serde_json::to_string_pretty(&data) |
||||
} else { |
||||
serde_json::to_string(&data) |
||||
}?.as_bytes()) |
||||
.await?; |
||||
Ok(()) |
||||
} |
||||
} |
||||
|
||||
#[derive(Debug, Error)] |
||||
pub enum GroupError { |
||||
#[error("Operation refused because the user is admin")] |
||||
UserIsAdmin, |
||||
#[error("Operation refused because the user is banned")] |
||||
UserIsBanned, |
||||
#[error("Server could not be banned because there are admin users on it")] |
||||
AdminsOnServer, |
||||
#[error("Group config is missing in the config store")] |
||||
GroupNotExist, |
||||
#[error(transparent)] |
||||
IoError(#[from] std::io::Error), |
||||
#[error(transparent)] |
||||
Serializer(#[from] serde_json::Error), |
||||
#[error(transparent)] |
||||
Elefren(#[from] elefren::Error), |
||||
} |
||||
|
||||
// this is for tests
|
||||
impl PartialEq for GroupError { |
||||
fn eq(&self, other: &Self) -> bool { |
||||
match (self, other) { |
||||
(Self::UserIsAdmin, Self::UserIsAdmin) => true, |
||||
(Self::UserIsBanned, Self::UserIsBanned) => true, |
||||
(Self::AdminsOnServer, Self::AdminsOnServer) => true, |
||||
(Self::GroupNotExist, Self::GroupNotExist) => true, |
||||
_ => false, |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn make_scopes() -> Scopes { |
||||
Scopes::read(scopes::Read::Accounts) |
||||
| Scopes::read(scopes::Read::Notifications) |
||||
| Scopes::read(scopes::Read::Statuses) |
||||
| Scopes::read(scopes::Read::Follows) |
||||
| Scopes::write(scopes::Write::Statuses) |
||||
| Scopes::write(scopes::Write::Media) |
||||
} |
||||
|
||||
// trait TapOk<T> {
|
||||
// fn tap_ok<F: FnOnce(&T)>(self, f: F) -> Self;
|
||||
// }
|
||||
//
|
||||
// impl<T, E> TapOk<T> for Result<T, E> {
|
||||
// fn tap_ok<F: FnOnce(&T)>(self, f: F) -> Self {
|
||||
// match self {
|
||||
// Ok(v) => {
|
||||
// f(&v);
|
||||
// Ok(v)
|
||||
// }
|
||||
// Err(e) => Err(e)
|
||||
// }
|
||||
// }
|
||||
// }
|
@ -0,0 +1,16 @@ |
||||
use std::error::Error; |
||||
|
||||
pub trait LogError { |
||||
fn log_error<S : AsRef<str>>(self, msg: S); |
||||
} |
||||
|
||||
impl<V, E : Error> LogError for Result<V, E> { |
||||
fn log_error<S : AsRef<str>>(self, msg: S) { |
||||
match self { |
||||
Ok(_) => {} |
||||
Err(e) => { |
||||
error!("{}: {}", msg.as_ref(), e); |
||||
} |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue