Update user information if needed

When a remote is displayed, if it has not been updated since at least 24 hours, newer informations are fetched.

Fixes #135
This commit is contained in:
Bat 2018-09-03 19:53:20 +01:00
parent 3373bb66cd
commit 32a4949f25
6 changed files with 52 additions and 7 deletions

View file

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE users DROP COLUMN last_fetched_date;

View file

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE users ADD COLUMN last_fetched_date TIMESTAMP NOT NULL DEFAULT '2000-01-01 00:00:01';

View file

@ -156,6 +156,7 @@ table! {
shared_inbox_url -> Nullable<Varchar>, shared_inbox_url -> Nullable<Varchar>,
followers_endpoint -> Varchar, followers_endpoint -> Varchar,
avatar_id -> Nullable<Int4>, avatar_id -> Nullable<Int4>,
last_fetched_date -> Timestamp,
} }
} }

View file

@ -5,7 +5,7 @@ use activitypub::{
object::Image, object::Image,
}; };
use bcrypt; use bcrypt;
use chrono::NaiveDateTime; use chrono::{NaiveDateTime, Utc};
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods, BelongingToDsl, PgConnection, dsl::any}; use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods, BelongingToDsl, PgConnection, dsl::any};
use openssl::{ use openssl::{
hash::MessageDigest, hash::MessageDigest,
@ -68,6 +68,7 @@ pub struct User {
pub shared_inbox_url: Option<String>, pub shared_inbox_url: Option<String>,
pub followers_endpoint: String, pub followers_endpoint: String,
pub avatar_id: Option<i32>, pub avatar_id: Option<i32>,
pub last_fetched_date: NaiveDateTime
} }
#[derive(Insertable)] #[derive(Insertable)]
@ -158,7 +159,7 @@ impl User {
} }
} }
pub fn fetch_from_url(conn: &PgConnection, url: String) -> Option<User> { fn fetch(url: String) -> Option<CustomPerson> {
let req = Client::new() let req = Client::new()
.get(&url[..]) .get(&url[..])
.header(Accept(ap_accept_header().into_iter().map(|h| qitem(h.parse::<Mime>().expect("Invalid Content-Type"))).collect())) .header(Accept(ap_accept_header().into_iter().map(|h| qitem(h.parse::<Mime>().expect("Invalid Content-Type"))).collect()))
@ -169,7 +170,7 @@ impl User {
if let Ok(ap_sign) = serde_json::from_str::<ApSignature>(text) { if let Ok(ap_sign) = serde_json::from_str::<ApSignature>(text) {
if let Ok(mut json) = serde_json::from_str::<CustomPerson>(text) { if let Ok(mut json) = serde_json::from_str::<CustomPerson>(text) {
json.custom_props = ap_sign; // without this workaround, publicKey is not correctly deserialized json.custom_props = ap_sign; // without this workaround, publicKey is not correctly deserialized
Some(User::from_activity(conn, json, Url::parse(url.as_ref()).unwrap().host_str().unwrap().to_string())) Some(json)
} else { None } } else { None }
} else { None } } else { None }
} else { None } } else { None }
@ -181,6 +182,10 @@ impl User {
} }
} }
pub fn fetch_from_url(conn: &PgConnection, url: String) -> Option<User> {
User::fetch(url.clone()).map(|json| (User::from_activity(conn, json, Url::parse(url.as_ref()).unwrap().host_str().unwrap().to_string())))
}
fn from_activity(conn: &PgConnection, acct: CustomPerson, inst: String) -> User { fn from_activity(conn: &PgConnection, acct: CustomPerson, inst: String) -> User {
let instance = match Instance::find_by_domain(conn, inst.clone()) { let instance = match Instance::find_by_domain(conn, inst.clone()) {
Some(instance) => instance, Some(instance) => instance,
@ -227,6 +232,26 @@ impl User {
user user
} }
pub fn refetch(&self, conn: &PgConnection) {
User::fetch(self.ap_url.clone()).map(|json| {
let avatar = Media::save_remote(conn, json.object.object_props.icon_image().expect("User::refetch: icon error")
.object_props.url_string().expect("User::refetch: icon.url error"));
diesel::update(self)
.set((
users::username.eq(json.object.ap_actor_props.preferred_username_string().expect("User::refetch: preferredUsername error")),
users::display_name.eq(json.object.object_props.name_string().expect("User::refetch: name error")),
users::outbox_url.eq(json.object.ap_actor_props.outbox_string().expect("User::refetch: outbox error")),
users::inbox_url.eq(json.object.ap_actor_props.inbox_string().expect("User::refetch: inbox error")),
users::summary.eq(SafeString::new(&json.object.object_props.summary_string().unwrap_or(String::new()))),
users::followers_endpoint.eq(json.object.ap_actor_props.followers_string().expect("User::refetch: followers error")),
users::avatar_id.eq(Some(avatar.id)),
users::last_fetched_date.eq(Utc::now().naive_utc())
)).execute(conn)
.expect("Couldn't update user")
});
}
pub fn hash_pass(pass: String) -> String { pub fn hash_pass(pass: String) -> String {
bcrypt::hash(pass.as_str(), 10).unwrap() bcrypt::hash(pass.as_str(), 10).unwrap()
} }
@ -504,6 +529,10 @@ impl User {
.execute(conn) .execute(conn)
.expect("Couldn't update user avatar"); .expect("Couldn't update user avatar");
} }
pub fn needs_update(&self) -> bool {
(Utc::now().naive_utc() - self.last_fetched_date).num_days() > 1
}
} }
impl<'a, 'r> FromRequest<'a, 'r> for User { impl<'a, 'r> FromRequest<'a, 'r> for User {

View file

@ -29,6 +29,7 @@ extern crate validator_derive;
extern crate webfinger; extern crate webfinger;
extern crate workerpool; extern crate workerpool;
use rocket::State;
use rocket_contrib::Template; use rocket_contrib::Template;
use rocket_csrf::CsrfFairingBuilder; use rocket_csrf::CsrfFairingBuilder;
use workerpool::{Pool, thunk::ThunkWorker}; use workerpool::{Pool, thunk::ThunkWorker};
@ -37,6 +38,8 @@ mod inbox;
mod setup; mod setup;
mod routes; mod routes;
type Worker<'a> = State<'a, Pool<ThunkWorker<()>>>;
fn main() { fn main() {
let pool = setup::check(); let pool = setup::check();
rocket::ignite() rocket::ignite()

View file

@ -5,7 +5,6 @@ use activitypub::{
}; };
use atom_syndication::{Entry, FeedBuilder}; use atom_syndication::{Entry, FeedBuilder};
use rocket::{ use rocket::{
State,
request::LenientForm, request::LenientForm,
response::{Redirect, Flash, Content}, response::{Redirect, Flash, Content},
http::ContentType http::ContentType
@ -13,7 +12,7 @@ use rocket::{
use rocket_contrib::Template; use rocket_contrib::Template;
use serde_json; use serde_json;
use validator::{Validate, ValidationError}; use validator::{Validate, ValidationError};
use workerpool::{Pool, thunk::*}; use workerpool::thunk::*;
use plume_common::activity_pub::{ use plume_common::activity_pub::{
ActivityStream, broadcast, Id, IntoId, ApRequest, ActivityStream, broadcast, Id, IntoId, ApRequest,
@ -31,6 +30,7 @@ use plume_models::{
}; };
use inbox::Inbox; use inbox::Inbox;
use routes::Page; use routes::Page;
use Worker;
#[get("/me")] #[get("/me")]
fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> { fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
@ -41,7 +41,7 @@ fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
} }
#[get("/@/<name>", rank = 2)] #[get("/@/<name>", rank = 2)]
fn details<'r>(name: String, conn: DbConn, account: Option<User>, worker: State<Pool<ThunkWorker<()>>>, fecth_articles_conn: DbConn, fecth_followers_conn: DbConn) -> Template { fn details(name: String, conn: DbConn, account: Option<User>, worker: Worker, fecth_articles_conn: DbConn, fecth_followers_conn: DbConn, update_conn: DbConn) -> Template {
may_fail!(account.map(|a| a.to_json(&*conn)), User::find_by_fqn(&*conn, name), "Couldn't find requested user", |user| { may_fail!(account.map(|a| a.to_json(&*conn)), User::find_by_fqn(&*conn, name), "Couldn't find requested user", |user| {
let recents = Post::get_recents_for_author(&*conn, &user, 6); let recents = Post::get_recents_for_author(&*conn, &user, 6);
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6); let reshares = Reshare::get_recents_for_author(&*conn, &user, 6);
@ -75,6 +75,14 @@ fn details<'r>(name: String, conn: DbConn, account: Option<User>, worker: State<
}); });
} }
})); }));
// Update profile information if needed
let user_clone = user.clone();
if user.needs_update() {
worker.execute(Thunk::of(move || {
user_clone.refetch(&*update_conn);
}))
}
} }
Template::render("users/details", json!({ Template::render("users/details", json!({
@ -106,7 +114,7 @@ fn dashboard_auth() -> Flash<Redirect> {
} }
#[get("/@/<name>/follow")] #[get("/@/<name>/follow")]
fn follow(name: String, conn: DbConn, user: User, worker: State<Pool<ThunkWorker<()>>>) -> Redirect { fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Redirect {
let target = User::find_by_fqn(&*conn, name.clone()).unwrap(); let target = User::find_by_fqn(&*conn, name.clone()).unwrap();
let f = follows::Follow::insert(&*conn, follows::NewFollow { let f = follows::Follow::insert(&*conn, follows::NewFollow {
follower_id: user.id, follower_id: user.id,