Add a fqn field to blogs and users (#457)

Fixes #319
This commit is contained in:
Baptiste Gelez 2019-03-06 18:28:10 +01:00 committed by GitHub
parent eff2698664
commit fe6e69d7c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 258 additions and 141 deletions

View File

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

View File

@ -0,0 +1,18 @@
-- Your SQL goes here
ALTER TABLE blogs ADD COLUMN fqn TEXT NOT NULL DEFAULT '';
UPDATE blogs SET fqn =
(CASE WHEN (SELECT local FROM instances WHERE id = instance_id) THEN
actor_id
ELSE
(actor_id || '@' || (SELECT public_domain FROM instances WHERE id = instance_id LIMIT 1))
END)
WHERE fqn = '';
ALTER TABLE users ADD COLUMN fqn TEXT NOT NULL DEFAULT '';
UPDATE users SET fqn =
(CASE WHEN (SELECT local FROM instances WHERE id = instance_id) THEN
username
ELSE
(username || '@' || (SELECT public_domain FROM instances WHERE id = instance_id LIMIT 1))
END)
WHERE fqn = '';

View File

@ -0,0 +1,77 @@
-- This file should undo anything in `up.sql`
CREATE TABLE blogs_no_fqn (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
actor_id VARCHAR NOT NULL,
title VARCHAR NOT NULL,
summary TEXT NOT NULL DEFAULT '',
outbox_url VARCHAR NOT NULL UNIQUE,
inbox_url VARCHAR NOT NULL UNIQUE,
instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL,
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
ap_url text not null default '' UNIQUE,
private_key TEXT,
public_key TEXT NOT NULL DEFAULT '',
CONSTRAINT blog_unique UNIQUE (actor_id, instance_id)
);
INSERT INTO blogs_no_fqn SELECT
id,
actor_id,
title,
summary,
outbox_url,
inbox_url,
instance_id,
creation_date,
ap_url,
private_key,
public_key
FROM blogs;
DROP TABLE blogs;
ALTER TABLE blogs_no_fqn RENAME TO blogs;
CREATE TABLE users_no_fqn (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
username VARCHAR NOT NULL,
display_name VARCHAR NOT NULL DEFAULT '',
outbox_url VARCHAR NOT NULL UNIQUE,
inbox_url VARCHAR NOT NULL UNIQUE,
is_admin BOOLEAN NOT NULL DEFAULT 'f',
summary TEXT NOT NULL DEFAULT '',
email TEXT,
hashed_password TEXT,
instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL,
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
ap_url TEXT NOT NULL default '' UNIQUE,
private_key TEXT,
public_key TEXT NOT NULL DEFAULT '',
shared_inbox_url VARCHAR,
followers_endpoint VARCHAR NOT NULL DEFAULT '' UNIQUE,
avatar_id INTEGER REFERENCES medias(id) ON DELETE SET NULL,
last_fetched_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT blog_authors_unique UNIQUE (username, instance_id)
);
INSERT INTO users_no_fqn SELECT
id,
username,
display_name,
outbox_url,
inbox_url,
is_admin,
summary,
email,
hashed_password,
instance_id,
creation_date,
ap_url,
private_key,
public_key,
shared_inbox_url,
followers_endpoint,
avatar_id,
last_fetched_date
FROM users;
DROP TABLE users;
ALTER TABLE users_no_fqn RENAME TO users;

View File

@ -0,0 +1,18 @@
-- Your SQL goes here
ALTER TABLE blogs ADD COLUMN fqn TEXT NOT NULL DEFAULT '';
UPDATE blogs SET fqn =
(CASE WHEN (SELECT local FROM instances WHERE id = instance_id) THEN
actor_id
ELSE
(actor_id || '@' || (SELECT public_domain FROM instances WHERE id = instance_id LIMIT 1))
END)
WHERE fqn = '';
ALTER TABLE users ADD COLUMN fqn TEXT NOT NULL DEFAULT '';
UPDATE users SET fqn =
(CASE WHEN (SELECT local FROM instances WHERE id = instance_id) THEN
username
ELSE
(username || '@' || (SELECT public_domain FROM instances WHERE id = instance_id LIMIT 1))
END)
WHERE fqn = '';

View File

@ -43,6 +43,7 @@ pub struct Blog {
pub ap_url: String,
pub private_key: Option<String>,
pub public_key: String,
pub fqn: String,
}
#[derive(Default, Insertable)]
@ -87,6 +88,15 @@ impl Blog {
"",
);
}
if inserted.fqn.is_empty() {
if instance.local {
inserted.fqn = inserted.actor_id.clone();
} else {
inserted.fqn = format!("{}@{}", inserted.actor_id, instance.public_domain);
}
}
inserted.save_changes(conn).map_err(Error::from)
});
get!(blogs);
@ -129,19 +139,17 @@ impl Blog {
.map_err(Error::from)
}
pub fn find_local(conn: &Connection, name: &str) -> Result<Blog> {
Blog::find_by_name(conn, name, Instance::get_local(conn)?.id)
}
pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Result<Blog> {
let mut split_fqn = fqn.split('@');
let actor = split_fqn.next().ok_or(Error::InvalidValue)?;
if let Some(domain) = split_fqn.next() { // remote blog
Instance::find_by_domain(conn, domain)
.and_then(|instance| Blog::find_by_name(conn, actor, instance.id))
.or_else(|_| Blog::fetch_from_webfinger(conn, fqn))
} else { // local blog
Blog::find_local(conn, actor)
let from_db = blogs::table
.filter(blogs::fqn.eq(fqn))
.limit(1)
.load::<Blog>(conn)?
.into_iter()
.next();
if let Some(from_db) = from_db {
Ok(from_db)
} else {
Blog::fetch_from_webfinger(conn, fqn)
}
}
@ -338,18 +346,6 @@ impl Blog {
})
}
pub fn get_fqn(&self, conn: &Connection) -> String {
if self.instance_id == Instance::get_local(conn).ok().expect("Blog::get_fqn: local instance error").id {
self.actor_id.clone()
} else {
format!(
"{}@{}",
self.actor_id,
self.get_instance(conn).ok().expect("Blog::get_fqn: instance error").public_domain
)
}
}
pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
for post in Post::get_for_blog(conn, &self)? {
post.delete(&(conn, searcher))?;
@ -649,7 +645,7 @@ pub(crate) mod tests {
).unwrap();
assert_eq!(
Blog::find_local(conn, "SomeName").unwrap().id,
Blog::find_by_fqn(conn, "SomeName").unwrap().id,
blog.id
);
@ -673,7 +669,7 @@ pub(crate) mod tests {
).unwrap(),
).unwrap();
assert_eq!(blog.get_fqn(conn), "SomeName");
assert_eq!(blog.fqn, "SomeName");
Ok(())
});

View File

@ -71,7 +71,7 @@ impl Mention {
.set_href_string(user.ap_url.clone())?;
mention
.link_props
.set_name_string(format!("@{}", user.get_fqn(conn)))?;
.set_name_string(format!("@{}", user.fqn))?;
Ok(mention)
}

View File

@ -81,7 +81,7 @@ impl Notification {
pub fn get_url(&self, conn: &Connection) -> Option<String> {
match self.kind.as_ref() {
notification_kind::COMMENT => self.get_post(conn).and_then(|p| Some(format!("{}#comment-{}", p.url(conn).ok()?, self.object_id))),
notification_kind::FOLLOW => Some(format!("/@/{}/", self.get_actor(conn).ok()?.get_fqn(conn))),
notification_kind::FOLLOW => Some(format!("/@/{}/", self.get_actor(conn).ok()?.fqn)),
notification_kind::MENTION => Mention::get(conn, self.object_id).and_then(|mention|
mention.get_post(conn).and_then(|p| p.url(conn))
.or_else(|_| {

View File

@ -269,7 +269,7 @@ impl Post {
post.ap_url = ap_url(&format!(
"{}/~/{}/{}/",
*BASE_URL,
post.get_blog(conn)?.get_fqn(conn),
post.get_blog(conn)?.fqn,
post.slug
));
let _: Post = post.save_changes(conn)?;
@ -850,7 +850,7 @@ impl Post {
pub fn url(&self, conn: &Connection) -> Result<String> {
let blog = self.get_blog(conn)?;
Ok(format!("/~/{}/{}", blog.get_fqn(conn), self.slug))
Ok(format!("/~/{}/{}", blog.fqn, self.slug))
}
pub fn cover_url(&self, conn: &Connection) -> Option<String> {

View File

@ -43,6 +43,7 @@ table! {
ap_url -> Text,
private_key -> Nullable<Text>,
public_key -> Text,
fqn -> Text,
}
}
@ -201,6 +202,7 @@ table! {
followers_endpoint -> Varchar,
avatar_id -> Nullable<Int4>,
last_fetched_date -> Timestamp,
fqn -> Text,
}
}

View File

@ -144,7 +144,7 @@ impl Searcher {
let writer = writer.as_mut().unwrap();
writer.add_document(doc!(
post_id => i64::from(post.id),
author => post.get_authors(conn)?.into_iter().map(|u| u.get_fqn(conn)).join(" "),
author => post.get_authors(conn)?.into_iter().map(|u| u.fqn).join(" "),
creation_date => i64::from(post.creation_date.num_days_from_ce()),
instance => Instance::get(conn, post.get_blog(conn)?.instance_id)?.public_domain.clone(),
tag => Tag::for_post(conn, post.id)?.into_iter().map(|t| t.tag).join(" "),

View File

@ -64,6 +64,7 @@ pub struct User {
pub followers_endpoint: String,
pub avatar_id: Option<i32>,
pub last_fetched_date: NaiveDateTime,
pub fqn: String,
}
#[derive(Default, Insertable)]
@ -119,8 +120,7 @@ impl User {
if inserted.shared_inbox_url.is_none() {
inserted.shared_inbox_url = Some(ap_url(&format!(
"{}/inbox",
Instance::get_local(conn)?
.public_domain
instance.public_domain
)));
}
@ -132,6 +132,14 @@ impl User {
);
}
if inserted.fqn.is_empty() {
if instance.local {
inserted.fqn = inserted.username.clone();
} else {
inserted.fqn = format!("{}@{}", inserted.username, instance.public_domain);
}
}
inserted.save_changes(conn).map_err(Error::from)
});
get!(users);
@ -218,19 +226,17 @@ impl User {
.map_err(Error::from)
}
pub fn find_local(conn: &Connection, username: &str) -> Result<User> {
User::find_by_name(conn, username, Instance::get_local(conn)?.id)
}
pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Result<User> {
let mut split_fqn = fqn.split('@');
let username = split_fqn.next().ok_or(Error::InvalidValue)?;
if let Some(domain) = split_fqn.next() { // remote user
Instance::find_by_domain(conn, domain)
.and_then(|instance| User::find_by_name(conn, username, instance.id))
.or_else(|_| User::fetch_from_webfinger(conn, fqn))
} else { // local user
User::find_local(conn, username)
let from_db = users::table
.filter(users::fqn.eq(fqn))
.limit(1)
.load::<User>(conn)?
.into_iter()
.next();
if let Some(from_db) = from_db {
Ok(from_db)
} else {
User::fetch_from_webfinger(conn, fqn)
}
}
@ -518,18 +524,6 @@ impl User {
.collect::<Vec<serde_json::Value>>())
}
pub fn get_fqn(&self, conn: &Connection) -> String {
if self.instance_id == Instance::get_local(conn).ok().expect("User::get_fqn: instance error").id {
self.username.clone()
} else {
format!(
"{}@{}",
self.username,
self.get_instance(conn).ok().expect("User::get_fqn: instance error").public_domain
)
}
}
pub fn get_followers(&self, conn: &Connection) -> Result<Vec<User>> {
use schema::follows;
let follows = Follow::belonging_to(self).select(follows::follower_id);
@ -805,11 +799,11 @@ impl User {
(Utc::now().naive_utc() - self.last_fetched_date).num_days() > 1
}
pub fn name(&self, conn: &Connection) -> String {
pub fn name(&self) -> String {
if !self.display_name.is_empty() {
self.display_name.clone()
} else {
self.get_fqn(conn)
self.fqn.clone()
}
}
}
@ -987,7 +981,7 @@ pub(crate) mod tests {
);
assert_eq!(
test_user.id,
User::find_by_fqn(conn, &test_user.get_fqn(conn)).unwrap().id
User::find_by_fqn(conn, &test_user.fqn).unwrap().id
);
assert_eq!(
test_user.id,

View File

@ -317,8 +317,9 @@ msgstr ""
msgid "Articles"
msgstr "المقالات"
#, fuzzy
msgid "Subscribers"
msgstr ""
msgstr "الوصف"
#, fuzzy
msgid "Subscriptions"

View File

@ -302,8 +302,9 @@ msgid ""
"can, however, find a different one."
msgstr ""
#, fuzzy
msgid "Articles"
msgstr ""
msgstr "Nueva publicación"
msgid "Subscribers"
msgstr ""

View File

@ -336,8 +336,9 @@ msgstr ""
msgid "Articles"
msgstr "Articles"
#, fuzzy
msgid "Subscribers"
msgstr ""
msgstr "Description"
#, fuzzy
msgid "Subscriptions"

View File

@ -332,8 +332,9 @@ msgstr ""
msgid "Articles"
msgstr "Artigos"
#, fuzzy
msgid "Subscribers"
msgstr ""
msgstr "Descrición"
#, fuzzy
msgid "Subscriptions"

View File

@ -335,8 +335,9 @@ msgstr ""
msgid "Articles"
msgstr "Articoli"
#, fuzzy
msgid "Subscribers"
msgstr ""
msgstr "Descrizione"
#, fuzzy
msgid "Subscriptions"

View File

@ -323,8 +323,9 @@ msgstr ""
msgid "Articles"
msgstr "記事"
#, fuzzy
msgid "Subscribers"
msgstr ""
msgstr "説明"
#, fuzzy
msgid "Subscriptions"

View File

@ -336,8 +336,9 @@ msgstr ""
msgid "Articles"
msgstr "artikler"
#, fuzzy
msgid "Subscribers"
msgstr ""
msgstr "Lang beskrivelse"
#, fuzzy
msgid "Subscriptions"

View File

@ -301,8 +301,9 @@ msgstr ""
msgid "Articles"
msgstr "Artykuły"
#, fuzzy
msgid "Subscribers"
msgstr ""
msgstr "Opis"
#, fuzzy
msgid "Subscriptions"

View File

@ -36,11 +36,11 @@ msgstr ""
msgid "{0}'s avatar"
msgstr ""
# src/routes/blogs.rs:65
# src/routes/blogs.rs:64
msgid "You need to be logged in order to create a new blog"
msgstr ""
# src/routes/blogs.rs:135
# src/routes/blogs.rs:134
msgid "You are not allowed to delete this blog."
msgstr ""

View File

@ -317,8 +317,9 @@ msgstr ""
msgid "Articles"
msgstr "Artigos"
#, fuzzy
msgid "Subscribers"
msgstr ""
msgstr "Descrição"
#, fuzzy
msgid "Subscriptions"

View File

@ -339,8 +339,9 @@ msgstr ""
msgid "Articles"
msgstr "Статьи"
#, fuzzy
msgid "Subscribers"
msgstr ""
msgstr "Описание"
#, fuzzy
msgid "Subscriptions"

View File

@ -49,7 +49,7 @@ pub struct OAuthRequest {
pub fn oauth(query: Form<OAuthRequest>, conn: DbConn) -> Result<Json<serde_json::Value>, ApiError> {
let app = App::find_by_client_id(&*conn, &query.client_id)?;
if app.client_secret == query.client_secret {
if let Ok(user) = User::find_local(&*conn, &query.username) {
if let Ok(user) = User::find_by_fqn(&*conn, &query.username) {
if user.auth(&query.password) {
let token = ApiToken::insert(&*conn, NewApiToken {
app_id: app.id,

View File

@ -34,7 +34,6 @@ pub fn details(intl: I18n, name: String, conn: DbConn, user: Option<User>, page:
Ok(render!(blogs::details(
&(&*conn, &intl.catalog, user.clone()),
blog.clone(),
blog.get_fqn(&*conn),
authors,
articles_count,
page.0,
@ -46,7 +45,7 @@ pub fn details(intl: I18n, name: String, conn: DbConn, user: Option<User>, page:
#[get("/~/<name>", rank = 1)]
pub fn activity_details(name: String, conn: DbConn, _ap: ApRequest) -> Option<ActivityStream<CustomGroup>> {
let blog = Blog::find_local(&*conn, &name).ok()?;
let blog = Blog::find_by_fqn(&*conn, &name).ok()?;
Some(ActivityStream::new(blog.to_activity(&*conn).ok()?))
}
@ -90,7 +89,7 @@ pub fn create(conn: DbConn, form: LenientForm<NewBlogForm>, user: User, intl: I1
Ok(_) => ValidationErrors::new(),
Err(e) => e
};
if Blog::find_local(&*conn, &slug).is_ok() {
if Blog::find_by_fqn(&*conn, &slug).is_ok() {
errors.add("title", ValidationError {
code: Cow::from("existing_slug"),
message: Some(Cow::from("A blog with the same name already exists.")),
@ -124,7 +123,7 @@ pub fn create(conn: DbConn, form: LenientForm<NewBlogForm>, user: User, intl: I1
#[post("/~/<name>/delete")]
pub fn delete(conn: DbConn, name: String, user: Option<User>, intl: I18n, searcher: Searcher) -> Result<Redirect, Ructe>{
let blog = Blog::find_local(&*conn, &name).expect("blog::delete: blog not found");
let blog = Blog::find_by_fqn(&*conn, &name).expect("blog::delete: blog not found");
if user.clone().and_then(|u| u.is_author_in(&*conn, &blog).ok()).unwrap_or(false) {
blog.delete(&conn, &searcher).expect("blog::expect: deletion error");
Ok(Redirect::to(uri!(super::instance::index)))
@ -139,7 +138,7 @@ pub fn delete(conn: DbConn, name: String, user: Option<User>, intl: I18n, search
#[get("/~/<name>/outbox")]
pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> {
let blog = Blog::find_local(&*conn, &name).ok()?;
let blog = Blog::find_by_fqn(&*conn, &name).ok()?;
Some(blog.outbox(&*conn).ok()?)
}

View File

@ -46,14 +46,14 @@ pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, res
warning: previous.clone().map(|p| p.spoiler_text).unwrap_or_default(),
content: previous.clone().and_then(|p| Some(format!(
"@{} {}",
p.get_author(&*conn).ok()?.get_fqn(&*conn),
p.get_author(&*conn).ok()?.fqn,
Mention::list_for_comment(&*conn, p.id).ok()?
.into_iter()
.filter_map(|m| {
let user = user.clone();
if let Ok(mentioned) = m.get_mentioned(&*conn) {
if user.is_none() || mentioned.id != user.expect("posts::details_response: user error while listing mentions").id {
Some(format!("@{}", mentioned.get_fqn(&*conn)))
Some(format!("@{}", mentioned.fqn))
} else {
None
}

View File

@ -40,7 +40,7 @@ pub struct LoginForm {
#[post("/login", data = "<form>")]
pub fn create(conn: DbConn, form: LenientForm<LoginForm>, flash: Option<FlashMessage>, mut cookies: Cookies, intl: I18n) -> Result<Redirect, Ructe> {
let user = User::find_by_email(&*conn, &form.email_or_name)
.or_else(|_| User::find_local(&*conn, &form.email_or_name));
.or_else(|_| User::find_by_fqn(&*conn, &form.email_or_name));
let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(),
Err(e) => e

View File

@ -203,7 +203,7 @@ pub fn activity_details(
conn: DbConn,
_ap: ApRequest,
) -> Option<ActivityStream<CustomPerson>> {
let user = User::find_local(&*conn, &name).ok()?;
let user = User::find_by_fqn(&*conn, &name).ok()?;
Some(ActivityStream::new(user.to_activity(&*conn).ok()?))
}
@ -369,7 +369,7 @@ pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Resul
#[get("/@/<name>/outbox")]
pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> {
let user = User::find_local(&*conn, &name).ok()?;
let user = User::find_by_fqn(&*conn, &name).ok()?;
user.outbox(&*conn).ok()
}
@ -381,7 +381,7 @@ pub fn inbox(
headers: Headers,
searcher: Searcher,
) -> Result<String, Option<status::BadRequest<&'static str>>> {
let user = User::find_local(&*conn, &name).map_err(|_| None)?;
let user = User::find_by_fqn(&*conn, &name).map_err(|_| None)?;
let act = data.1.into_inner();
let sig = data.0;
@ -429,7 +429,7 @@ pub fn ap_followers(
conn: DbConn,
_ap: ApRequest,
) -> Option<ActivityStream<OrderedCollection>> {
let user = User::find_local(&*conn, &name).ok()?;
let user = User::find_by_fqn(&*conn, &name).ok()?;
let followers = user
.get_followers(&*conn).ok()?
.into_iter()

View File

@ -39,9 +39,9 @@ impl Resolver<DbConn> for WebfingerResolver {
}
fn find(acct: String, conn: DbConn) -> Result<Webfinger, ResolverError> {
User::find_local(&*conn, &acct)
User::find_by_fqn(&*conn, &acct)
.and_then(|usr| usr.webfinger(&*conn))
.or_else(|_| Blog::find_local(&*conn, &acct)
.or_else(|_| Blog::find_by_fqn(&*conn, &acct)
.and_then(|blog| blog.webfinger(&*conn))
.or(Err(ResolverError::NotFound)))
}

View File

@ -29,7 +29,7 @@ macro_rules! render {
}
pub fn translate_notification(ctx: BaseContext, notif: Notification) -> String {
let name = notif.get_actor(ctx.0).unwrap().name(ctx.0);
let name = notif.get_actor(ctx.0).unwrap().name();
match notif.kind.as_ref() {
notification_kind::COMMENT => i18n!(ctx.1, "{0} commented your article."; &name),
notification_kind::FOLLOW => i18n!(ctx.1, "{0} is subscribed to you."; &name),
@ -55,7 +55,7 @@ impl Size {
}
pub fn avatar(conn: &Connection, user: &User, size: Size, pad: bool, catalog: &Catalog) -> Html<String> {
let name = escape(&user.name(conn)).to_string();
let name = escape(&user.name()).to_string();
Html(format!(
r#"<div class="avatar {size} {padded}"
style="background-image: url('{url}');"

View File

@ -5,26 +5,26 @@
@use template_utils::*;
@use routes::*;
@(ctx: BaseContext, blog: Blog, fqn: String, authors: &Vec<User>, total_articles: i64, page: i32, n_pages: i32, is_author: bool, posts: Vec<Post>)
@(ctx: BaseContext, blog: Blog, authors: &Vec<User>, total_articles: i64, page: i32, n_pages: i32, is_author: bool, posts: Vec<Post>)
@:base(ctx, blog.title.clone(), {}, {
<a href="@uri!(blogs::details: name = &fqn, page = _)">@blog.title</a>
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)">@blog.title</a>
}, {
<div class="hidden">
@for author in authors {
<div class="h-card">
<span class="p-name">@author.name(ctx.0)</span>
<span class="p-name">@author.name()</span>
<a class="u-url" href="@author.ap_url"></a>
</div>
}
</div>
<div class="h-feed">
<h1><span class="p-name">@blog.title</span> <small>~@fqn</small></h1>
<h1><span class="p-name">@blog.title</span> <small>~@blog.fqn</small></h1>
<p>@blog.summary</p>
<p>
@i18n!(ctx.1, "There's one author on this blog: ", "There are {0} authors on this blog: "; authors.len())
@for author in authors {
<a class="author p-author" href="@uri!(user::details: name = author.get_fqn(ctx.0))">@author.name(ctx.0)</a>
<a class="author p-author" href="@uri!(user::details: name = &author.fqn)">@author.name()</a>
}
</p>
<p>
@ -34,13 +34,13 @@
<section>
<h2>
@i18n!(ctx.1, "Latest articles")
<small><a href="@uri!(blogs::atom_feed: name = &fqn)" title="Atom feed">@icon!("rss")</a></small>
<small><a href="@uri!(blogs::atom_feed: name = &blog.fqn)" title="Atom feed">@icon!("rss")</a></small>
</h2>
@if posts.len() < 1 {
<p>@i18n!(ctx.1, "No posts to see here yet.")</p>
}
@if is_author {
<a href="@uri!(posts::new: blog = &fqn)" class="button inline-block">@i18n!(ctx.1, "New article")</a>
<a href="@uri!(posts::new: blog = &blog.fqn)" class="button inline-block">@i18n!(ctx.1, "New article")</a>
}
<div class="cards">
@for article in posts {
@ -53,7 +53,7 @@
@if is_author {
<h2>@i18n!(ctx.1, "Danger zone")</h2>
<p>@i18n!(ctx.1, "Be very careful, any action taken here can't be reversed.")</p>
<form method="post" action="@uri!(blogs::delete: name = &fqn)">
<form method="post" action="@uri!(blogs::delete: name = &blog.fqn)">
<input type="submit" class="inline-block button destructive" value="@i18n!(ctx.1, "Permanently delete this blog")">
</form>
}

View File

@ -24,7 +24,7 @@
<div>
<p>@i18n!(ctx.1, "Administred by")</p>
@avatar(ctx.0, &admin, Size::Small, false, ctx.1)
<p><a href="@uri!(user::details: name = admin.get_fqn(ctx.0))">@admin.name(ctx.0)</a><small>@@@admin.get_fqn(ctx.0)</small></p>
<p><a href="@uri!(user::details: name = &admin.fqn)">@admin.name()</a><small>@@@admin.fqn</small></p>
</div>
</section>
<p>@i18n!(ctx.1, "Runs Plume {0}"; env!("CARGO_PKG_VERSION"))</p>

View File

@ -9,9 +9,9 @@
<h1>@i18n!(ctx.1, "Users")</h1>
@tabs(&[
(&uri!(instance::admin).to_string(), i18n!(ctx.1, "Configuration"), false),
(&uri!(instance::admin_instances: page = _).to_string(), i18n!(ctx.1, "Instances"), false),
(&uri!(instance::admin_users: page = _).to_string(), i18n!(ctx.1, "Users"), true),
(&uri!(instance::admin).to_string(), i18n!(ctx.1, "Configuration"), false),
(&uri!(instance::admin_instances: page = _).to_string(), i18n!(ctx.1, "Instances"), false),
(&uri!(instance::admin_users: page = _).to_string(), i18n!(ctx.1, "Users"), true),
])
<div class="list">
@ -19,7 +19,7 @@
<div class="flex">
@avatar(ctx.0, &user, Size::Small, false, ctx.1)
<p class="grow">
<a href="@uri!(user::details: name = user.get_fqn(ctx.0))">@user.name(ctx.0)</a>
<a href="@uri!(user::details: name = &user.fqn)">@user.name()</a>
<small>@format!("@{}", user.username)</small>
</p>
@if !user.is_admin {

View File

@ -7,10 +7,10 @@
@if let Some(ref comm) = Some(&comment_tree.comment) {
@if let Some(author) = comm.get_author(ctx.0).ok() {
<div class="comment u-comment h-cite" id="comment-@comm.id">
<a class="author u-author h-card" href="@uri!(user::details: name = author.get_fqn(ctx.0))">
<a class="author u-author h-card" href="@uri!(user::details: name = &author.fqn)">
@avatar(ctx.0, &author, Size::Small, true, ctx.1)
<span class="display-name p-name">@author.name(ctx.0)</span>
<small>@author.get_fqn(ctx.0)</small>
<span class="display-name p-name">@author.name()</span>
<small>@&author.fqn</small>
</a>
@if let Some(ref ap_url) = comm.ap_url {
<a class="u-url" href="@ap_url"></a>

View File

@ -9,7 +9,7 @@
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
}
<h3 class="p-name">
<a class="u-url" href="@uri!(posts::details: blog = article.get_blog(ctx.0).unwrap().get_fqn(ctx.0), slug = &article.slug, responding_to = _)">
<a class="u-url" href="@uri!(posts::details: blog = article.get_blog(ctx.0).unwrap().fqn, slug = &article.slug, responding_to = _)">
@article.title
</a>
</h3>
@ -19,15 +19,15 @@
<footer class="authors">
@Html(i18n!(ctx.1, "By {0}"; format!(
"<a class=\"p-author h-card\" href=\"{}\">{}</a>",
uri!(user::details: name = article.get_authors(ctx.0).unwrap_or_default()[0].get_fqn(ctx.0)),
escape(&article.get_authors(ctx.0).unwrap_or_default()[0].name(ctx.0))
uri!(user::details: name = &article.get_authors(ctx.0).unwrap_or_default()[0].fqn),
escape(&article.get_authors(ctx.0).unwrap_or_default()[0].name())
)))
@if article.published {
<span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span>
<span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span>
}
<a href="@uri!(blogs::details: name = article.get_blog(ctx.0).unwrap().get_fqn(ctx.0), page = _)">@article.get_blog(ctx.0).unwrap().title</a>
<a href="@uri!(blogs::details: name = &article.get_blog(ctx.0).unwrap().fqn, page = _)">@article.get_blog(ctx.0).unwrap().title</a>
@if !article.published {
⋅ @i18n!(ctx.1, "Draft")
⋅ @i18n!(ctx.1, "Draft")
}
</footer>
</div>

View File

@ -17,10 +17,10 @@
@if article.cover_id.is_some() {
<meta property="og:image" content="@Html(article.cover_url(ctx.0).unwrap_or_default())"/>
}
<meta property="og:url" content="@uri!(posts::details: blog = blog.get_fqn(ctx.0), slug = &article.slug, responding_to = _)"/>
<meta property="og:url" content="@uri!(posts::details: blog = &blog.fqn, slug = &article.slug, responding_to = _)"/>
<meta property="og:description" content="@article.subtitle"/>
}, {
<a href="@uri!(blogs::details: name = blog.get_fqn(ctx.0), page = _)">@blog.title</a>
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)">@blog.title</a>
}, {
<div class="h-entry">
<h1 class="article p-name">@&article.title</h1>
@ -28,16 +28,16 @@
<div class="article-info">
<span class="author">
@Html(i18n!(ctx.1, "Written by {0}"; format!("<a href=\"{}\">{}</a>",
uri!(user::details: name = &author.get_fqn(ctx.0)),
escape(&author.name(ctx.0)))))
uri!(user::details: name = &author.fqn),
escape(&author.name()))))
</span>
&mdash;
<span class="date dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span><a class="u-url" href="@article.ap_url"></a>
@if ctx.2.clone().map(|u| u.id == author.id).unwrap_or(false) {
&mdash;
<a href="@uri!(posts::edit: blog = blog.get_fqn(ctx.0), slug = &article.slug)">@i18n!(ctx.1, "Edit")</a>
<a href="@uri!(posts::edit: blog = &blog.fqn, slug = &article.slug)">@i18n!(ctx.1, "Edit")</a>
&mdash;
<form class="inline" method="post" action="@uri!(posts::delete: blog_name = blog.get_fqn(ctx.0), slug = &article.slug)">
<form class="inline" method="post" action="@uri!(posts::delete: blog_name = &blog.fqn, slug = &article.slug)">
<input onclick="return confirm('Are you sure you?')" type="submit" value="@i18n!(ctx.1, "Delete this article")">
</form>
}
@ -73,20 +73,20 @@
@avatar(ctx.0, &author, Size::Medium, true, ctx.1)
<div class="grow">
<h2 class="p-name">
<a href="@uri!(user::details: name = author.get_fqn(ctx.0))">@author.name(ctx.0)</a>
<a href="@uri!(user::details: name = &author.fqn)">@author.name()</a>
<a rel="author" class="u-url" href="@author.ap_url"></a>
</h2>
<p>@Html(&author.summary)</h2>
</div>
@if !ctx.2.as_ref().map(|u| u.id == author.id).unwrap_or(false) {
<form action="@uri!(user::follow: name = author.get_fqn(ctx.0))" method="POST">
<form action="@uri!(user::follow: name = &author.fqn)" method="POST">
<input type="submit" class="button" value="@if is_following {@i18n!(ctx.1, "Unsubscribe")} else {@i18n!(ctx.1, "Subscribe")}">
</form>
}
</div>
@if ctx.2.is_some() {
<div class="actions">
<form class="likes" action="@uri!(likes::create: blog = blog.get_fqn(ctx.0), slug = &article.slug)" method="POST">
<form class="likes" action="@uri!(likes::create: blog = &blog.fqn, slug = &article.slug)" method="POST">
<p aria-label="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)" title="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)">
@n_likes
</p>
@ -97,7 +97,7 @@
<button type="submit" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</button>
}
</form>
<form class="reshares" action="@uri!(reshares::create: blog = blog.get_fqn(ctx.0), slug = &article.slug)" method="POST">
<form class="reshares" action="@uri!(reshares::create: blog = &blog.fqn, slug = &article.slug)" method="POST">
<p aria-label="@i18n!(ctx.1, "One boost", "{0} boost"; n_reshares)" title="@i18n!(ctx.1, "One boost", "{0} boosts"; n_reshares)">
@n_reshares
</p>
@ -131,7 +131,7 @@
<h2>@i18n!(ctx.1, "Comments")</h2>
@if ctx.2.is_some() {
<form method="post" action="@uri!(comments::create: blog_name = blog.get_fqn(ctx.0), slug = &article.slug)">
<form method="post" action="@uri!(comments::create: blog_name = &blog.fqn, slug = &article.slug)">
@input!(ctx.1, warning (optional text), "Content warning", comment_form, comment_errors, "")
<label for="plume-editor">@i18n!(ctx.1, "Your comment")</label>
@ -146,7 +146,7 @@
@if !comments.is_empty() {
<div class="list">
@for comm in comments {
@:comment(ctx, &comm, Some(&article.ap_url), &blog.get_fqn(ctx.0), &article.slug)
@:comment(ctx, &comm, Some(&article.ap_url), &blog.fqn, &article.slug)
}
</div>
} else {

View File

@ -6,20 +6,20 @@
@(ctx: BaseContext, user: User, follows: bool, is_remote: bool, remote_url: String, recents: Vec<Post>, reshares: Vec<Post>)
@:base(ctx, user.name(ctx.0), {}, {}, {
@:base(ctx, user.name(), {}, {}, {
@:header(ctx, &user, follows, is_remote, remote_url)
@tabs(&[
(&uri!(user::details: name= user.get_fqn(ctx.0)).to_string(), i18n!(ctx.1, "Articles"), true),
(&uri!(user::followers: name = user.get_fqn(ctx.0), page = _).to_string(), i18n!(ctx.1, "Subscribers"), false),
(&uri!(user::followed: name = user.get_fqn(ctx.0), page = _).to_string(), i18n!(ctx.1, "Subscriptions"), false)
(&uri!(user::details: name = &user.fqn).to_string(), i18n!(ctx.1, "Articles"), true),
(&uri!(user::followers: name = &user.fqn, page = _).to_string(), i18n!(ctx.1, "Subscribers"), false),
(&uri!(user::followed: name = &user.fqn, page = _).to_string(), i18n!(ctx.1, "Subscriptions"), false)
])
@if !recents.is_empty() {
<div class="h-feed">
<h2>
<span class="p-name">@i18n!(ctx.1, "Latest articles")</span>
<small><a href="@uri!(user::atom_feed: name = user.get_fqn(ctx.0))" title="@i18n!(ctx.1, "Atom feed")">@icon!("rss")</a></small>
<small><a href="@uri!(user::atom_feed: name = &user.fqn))" title="@i18n!(ctx.1, "Atom feed")">@icon!("rss")</a></small>
</h2>
<div class="cards">
@for article in recents {

View File

@ -5,19 +5,19 @@
@(ctx: BaseContext, user: User, follows: bool, is_remote: bool, remote_url: String, followed: Vec<User>, page: i32, n_pages: i32)
@:base(ctx, i18n!(ctx.1, "{0}'s subscriptions'"; user.name(ctx.0)), {}, {}, {
@:base(ctx, i18n!(ctx.1, "{0}'s subscriptions'"; user.name()), {}, {}, {
@:header(ctx, &user, follows, is_remote, remote_url)
@tabs(&[
(&uri!(user::details: name= user.get_fqn(ctx.0)).to_string(), i18n!(ctx.1, "Articles"), true),
(&uri!(user::followers: name = user.get_fqn(ctx.0), page = _).to_string(), i18n!(ctx.1, "Subscribers"), false),
(&uri!(user::followed: name = user.get_fqn(ctx.0), page = _).to_string(), i18n!(ctx.1, "Subscriptions"), false)
(&uri!(user::details: name = &user.fqn).to_string(), i18n!(ctx.1, "Articles"), true),
(&uri!(user::followers: name = &user.fqn, page = _).to_string(), i18n!(ctx.1, "Subscribers"), false),
(&uri!(user::followed: name = &user.fqn, page = _).to_string(), i18n!(ctx.1, "Subscriptions"), false)
])
<div class="cards">
@for follow in followed {
<div class="card">
<h3><a href="@uri!(user::details: name = follow.get_fqn(ctx.0))">@follow.name(ctx.0)</a> <small>@format!("@{}", follow.get_fqn(ctx.0))</small></h3>
<h3><a href="@uri!(user::details: name = &follow.fqn)">@follow.name()</a> <small>@format!("@{}", &follow.fqn)</small></h3>
<main><p>@Html(follow.summary)</p></main>
</div>
}

View File

@ -5,19 +5,19 @@
@(ctx: BaseContext, user: User, follows: bool, is_remote: bool, remote_url: String, followers: Vec<User>, page: i32, n_pages: i32)
@:base(ctx, i18n!(ctx.1, "{0}'s subscribers"; user.name(ctx.0)), {}, {}, {
@:base(ctx, i18n!(ctx.1, "{0}'s subscribers"; user.name()), {}, {}, {
@:header(ctx, &user, follows, is_remote, remote_url)
@tabs(&[
(&uri!(user::details: name= user.get_fqn(ctx.0)).to_string(), i18n!(ctx.1, "Articles"), true),
(&uri!(user::followers: name = user.get_fqn(ctx.0), page = _).to_string(), i18n!(ctx.1, "Subscribers"), false),
(&uri!(user::followed: name = user.get_fqn(ctx.0), page = _).to_string(), i18n!(ctx.1, "Subscriptions"), false)
(&uri!(user::details: name = &user.fqn).to_string(), i18n!(ctx.1, "Articles"), true),
(&uri!(user::followers: name = &user.fqn, page = _).to_string(), i18n!(ctx.1, "Subscribers"), false),
(&uri!(user::followed: name = &user.fqn, page = _).to_string(), i18n!(ctx.1, "Subscriptions"), false)
])
<div class="cards">
@for follower in followers {
<div class="card">
<h3><a href="@uri!(user::details: name = follower.get_fqn(ctx.0))">@follower.name(ctx.0)</a> <small>@format!("@{}", follower.get_fqn(ctx.0))</small></h3>
<h3><a href="@uri!(user::details: name = &follower.fqn)">@follower.name()</a> <small>@format!("@{}", &follower.fqn)</small></h3>
<main><p>@Html(follower.summary)</p></main>
</div>
}

View File

@ -10,8 +10,8 @@
@avatar(ctx.0, &user, Size::Medium, false, ctx.1)
<h1 class="grow flex vertical">
<span class="p-name">@user.name(ctx.0)</span>
<small class="p-nickname">@user.get_fqn(ctx.0)</small>
<span class="p-name">@user.name()</span>
<small class="p-nickname">@&user.fqn</small>
</h1>
<p>
@ -33,7 +33,7 @@
}
@if ctx.2.clone().map(|u| u.id != user.id).unwrap_or(false) {
<form class="inline" method="post" action="@uri!(user::follow: name = user.get_fqn(ctx.0))">
<form class="inline" method="post" action="@uri!(user::follow: name = &user.fqn)">
@if follows {
<input type="submit" value="@i18n!(ctx.1, "Unsubscribe")">
} else {