Merge branch 'master' of https://github.com/Plume-org/Plume into mobile-menu

This commit is contained in:
Madeorsk 2018-09-01 16:12:11 +02:00
commit ccab6107fa
75 changed files with 3662 additions and 745 deletions

112
Cargo.lock generated
View File

@ -210,7 +210,7 @@ dependencies = [
"num-integer 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -236,7 +236,7 @@ source = "git+https://github.com/alexcrichton/cookie-rs?rev=0365a18#0365a18e4518
dependencies = [
"base64 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"ring 0.13.0-alpha5 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -537,7 +537,7 @@ dependencies = [
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
@ -560,7 +560,7 @@ dependencies = [
"mime 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"relay 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-proto 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -592,6 +592,11 @@ dependencies = [
"unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "if_chain"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "indexmap"
version = "1.0.1"
@ -991,11 +996,16 @@ dependencies = [
"rocket 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)",
"rocket_codegen 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)",
"rocket_contrib 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)",
"rocket_csrf 0.1.0 (git+https://github.com/fdb-hiroshima/rocket_csrf?rev=13ca47ef73be86cef9caca30c516e4e95f3051ce)",
"rocket_csrf 0.1.0 (git+https://github.com/fdb-hiroshima/rocket_csrf?branch=plume)",
"rocket_i18n 0.1.1 (git+https://github.com/BaptisteGelez/rocket_i18n?rev=5b4225d5bed5769482dc926a7e6d6b79f1217be6)",
"rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
"webfinger 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"validator 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"validator_derive 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"webfinger 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"workerpool 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -1042,7 +1052,7 @@ dependencies = [
"serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"webfinger 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"webfinger 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -1183,6 +1193,18 @@ dependencies = [
"utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "regex"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
"memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-syntax 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
"thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "regex-syntax"
version = "0.5.5"
@ -1191,6 +1213,14 @@ dependencies = [
"ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "regex-syntax"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "relay"
version = "0.1.1"
@ -1256,7 +1286,7 @@ dependencies = [
"rocket_codegen_next 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)",
"rocket_http 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)",
"state 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"yansi 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1299,13 +1329,14 @@ dependencies = [
[[package]]
name = "rocket_csrf"
version = "0.1.0"
source = "git+https://github.com/fdb-hiroshima/rocket_csrf?rev=13ca47ef73be86cef9caca30c516e4e95f3051ce#13ca47ef73be86cef9caca30c516e4e95f3051ce"
source = "git+https://github.com/fdb-hiroshima/rocket_csrf?branch=plume#5309b7634a9cd204d003312cb70ce9d36b660e6b"
dependencies = [
"csrf 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"data-encoding 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"rocket 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)",
"serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -1318,8 +1349,8 @@ dependencies = [
"indexmap 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"pear 0.1.0 (git+http://github.com/SergioBenitez/Pear?rev=54667ae)",
"percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"smallvec 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
"smallvec 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -1352,7 +1383,7 @@ dependencies = [
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -1493,8 +1524,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "smallvec"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "state"
@ -1651,7 +1685,7 @@ dependencies = [
[[package]]
name = "time"
version = "0.1.39"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1926,6 +1960,32 @@ dependencies = [
"rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "validator"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "validator_derive"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"if_chain 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)",
"validator 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "vcpkg"
version = "0.2.3"
@ -1943,7 +2003,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "webfinger"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1981,6 +2041,14 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "workerpool"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ws2_32-sys"
version = "0.2.1"
@ -2064,6 +2132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum hyper 0.11.25 (registry+https://github.com/rust-lang/crates.io-index)" = "549dbb86397490ce69d908425b9beebc85bbaad25157d67479d4995bb56fdf9a"
"checksum hyper-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a5aa51f6ae9842239b0fac14af5f22123b8432b4cc774a44ff059fcba0f675ca"
"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d"
"checksum if_chain 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "61bb90bdd39e3af69b0172dfc6130f6cd6332bf040fbb9bdd4401d37adbd48b8"
"checksum indexmap 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "08173ba1e906efb6538785a8844dd496f5d34f0a2d88038e95195172fc667220"
"checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08"
"checksum isatty 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a118a53ba42790ef25c82bb481ecf36e2da892646cccd361e69a6bb881e19398"
@ -2128,7 +2197,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum rand_core 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "edecf0f94da5551fc9b492093e30b041a891657db7940ee221f9d2f66e82eef2"
"checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd"
"checksum regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "aec3f58d903a7d2a9dc2bf0e41a746f4530e0cab6b615494e058f67a3ef947fb"
"checksum regex 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "13c93d55961981ba9226a213b385216f83ab43bd6ac53ab16b2eeb47e337cf4e"
"checksum regex-syntax 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "bd90079345f4a4c3409214734ae220fd773c6f2e8a543d07370c6c1c369cfbfb"
"checksum regex-syntax 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05b06a75f5217880fc5e905952a42750bf44787e56a6c6d6852ed0992f5e1d54"
"checksum relay 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1576e382688d7e9deecea24417e350d3062d97e32e45d70b1cde65994ff1489a"
"checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5"
"checksum reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)" = "241faa9a8ca28a03cbbb9815a5d085f271d4c0168a19181f106aa93240c22ddb"
@ -2137,7 +2208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum rocket_codegen 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)" = "<none>"
"checksum rocket_codegen_next 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)" = "<none>"
"checksum rocket_contrib 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)" = "<none>"
"checksum rocket_csrf 0.1.0 (git+https://github.com/fdb-hiroshima/rocket_csrf?rev=13ca47ef73be86cef9caca30c516e4e95f3051ce)" = "<none>"
"checksum rocket_csrf 0.1.0 (git+https://github.com/fdb-hiroshima/rocket_csrf?branch=plume)" = "<none>"
"checksum rocket_http 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)" = "<none>"
"checksum rocket_i18n 0.1.1 (git+https://github.com/BaptisteGelez/rocket_i18n?rev=5b4225d5bed5769482dc926a7e6d6b79f1217be6)" = "<none>"
"checksum rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d127299b02abda51634f14025aec43ae87a7aa7a95202b6a868ec852607d1451"
@ -2161,7 +2232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum slab 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fdeff4cd9ecff59ec7e3744cbca73dfe5ac35c2aedb2cfba8a1c715a18912e9d"
"checksum slug 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "797bcb4d24e91239a8615415814f4afb2d8ca400c472de3c73f803a5a7689e11"
"checksum smallvec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4c8cbcd6df1e117c2210e13ab5109635ad68a929fcbb8964dc965b76cb5ee013"
"checksum smallvec 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "312a7df010092e73d6bbaf141957e868d4f30efd2bfd9bb1028ad91abec58514"
"checksum smallvec 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "26df3bb03ca5eac2e64192b723d51f56c1b1e0860e7c766281f4598f181acdc8"
"checksum state 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7345c971d1ef21ffdbd103a75990a15eb03604fc8b8852ca8cb418ee1a099028"
"checksum string_cache 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "25d70109977172b127fe834e5449e5ab1740b9ba49fa18a2020f509174f25423"
"checksum string_cache_codegen 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "35293b05cf1494e8ddd042a7df6756bf18d07f42d234f32e71dce8a7aabb0191"
@ -2178,7 +2249,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum tendril 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9de21546595a0873061940d994bbbc5c35f024ae4fd61ec5c5b159115684f508"
"checksum tera 0.11.7 (registry+https://github.com/rust-lang/crates.io-index)" = "e815b67d44c26feb06630011fb58b5b243f4e9585aac1ed0592c5795de64cd75"
"checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963"
"checksum time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "a15375f1df02096fb3317256ce2cee6a1f42fc84ea5ad5fc8c421cfe40c73098"
"checksum time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b"
"checksum tokio 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "be15ef40f675c9fe66e354d74c73f3ed012ca1aa14d65846a33ee48f1ae8d922"
"checksum tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "aeeffbbb94209023feaef3c196a41cbcdafa06b4a6f893f68779bb5e53796f71"
"checksum tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8cac2a7883ff3567e9d66bb09100d09b33d90311feca0206c7ca034bc0c55113"
@ -2210,14 +2281,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum utf-8 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1262dfab4c30d5cb7c07026be00ee343a6cf5027fdc0104a9160f354e5db75c"
"checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122"
"checksum uuid 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22"
"checksum validator 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4a8c44fecf027a477e70a86cd7f4863410adf120ca2cb13408cb099057b8e2d0"
"checksum validator_derive 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "708ee89305635499f793d0e2dd9d0b1b5d00daba90fdfb1392b87c7279521fab"
"checksum vcpkg 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7ed0f6789c8a85ca41bbc1c9d175422116a9869bd1cf31bb08e1493ecce60380"
"checksum version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6b772017e347561807c1aa192438c5fd74242a670a6cffacc40f2defd1dc069d"
"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
"checksum webfinger 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a3e10c73859f818558bbed7fc9bf69a939ebdfc86cba637c9e7eda4606088eae"
"checksum webfinger 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e34ffa5f00fedd30b71718a02b973df00a66e8059ad041bc597b455b22a2aba2"
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
"checksum winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "04e3bd221fcbe8a271359c04f21a76db7d0c6028862d1bb5512d85e1e2eb5bb3"
"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
"checksum workerpool 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4f49756646617bde19ff95b370cfa5c0f7ead17a90c90d7cb62dc31dfaa8c625"
"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
"checksum yansi 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d60c3b48c9cdec42fb06b3b84b5b087405e1fa1c644a1af3930e4dfafe93de48"

View File

@ -10,19 +10,24 @@ failure = "0.1"
gettext-rs = "0.4"
heck = "0.3.0"
rpassword = "2.0"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
webfinger = "0.2"
validator = "0.7"
validator_derive = "0.7"
webfinger = "0.3"
workerpool = "1.1"
[dependencies.diesel]
features = ["postgres", "r2d2", "chrono"]
version = "*"
[dependencies.plume-models]
path = "plume-models"
[dependencies.plume-common]
path = "plume-common"
[dependencies.plume-models]
path = "plume-models"
[dependencies.rocket]
git = "https://github.com/SergioBenitez/Rocket"
rev = "df7111143e466c18d1f56377a8d9530a5a306aba"
@ -37,12 +42,12 @@ git = "https://github.com/SergioBenitez/Rocket"
rev = "df7111143e466c18d1f56377a8d9530a5a306aba"
[dependencies.rocket_csrf]
branch = "plume"
git = "https://github.com/fdb-hiroshima/rocket_csrf"
rev = "13ca47ef73be86cef9caca30c516e4e95f3051ce"
[dependencies.rocket_i18n]
git = "https://github.com/BaptisteGelez/rocket_i18n"
rev = "5b4225d5bed5769482dc926a7e6d6b79f1217be6"
[workspace]
members = ['plume-models', 'plume-common']
members = ["plume-models", "plume-common"]

View File

@ -1,72 +0,0 @@
# How to install Plume on a Debian stretch:
## Basic setup:
```bash
apt update
apt install gettext postgresql postgresql-contrib libpq-dev
adduser plume
su - plume
cd /home/plume
git clone https://github.com/Plume-org/Plume.git
curl https://sh.rustup.rs -sSf | sh
cd Plume
rustup toolchain install nightly
rustup toolchain default nightly
rustup update
cargo install diesel_cli --no-default-features --features postgres # we dont need to compile anything else than pgsql
```
## Now, if you want to run postgresql on the same server:
```bash
service postgresql start
cargo run # this will configure and launch Plume on the server.
```
## If you want to run Plume with a remote DB this time ( Postgresql is not installed on the same server/container):
* On the DB server:
```bash
service postgresql start
su - postgres
createuser -d -P plume
createdb -O plume plume
```
* On the Plume server:
```bash
cd /home/plume/Plume
diesel migration run --database-url postgres://plume:PASSWORD@DBSERVERIP:DBPORT/plume
DB_URL=postgres://plume:PASSWORD@DBSERVERIP:DBPORT/plume cargo run # the first launch will ask questions to configure the instance. A second launch will not need the DB_URL.
```
## Plume is now accessible as seen on your console. You can have fun now, or configure an nginx proxy with the following excerpt:
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_pass http://localhost:8000;
client_max_body_size 16m;
}
# Caveats:
* Pgbouncer is not yet supported ( named transactions are used ).
* Rust nightly is a moving target, dependancies can break and sometimes you need to check a few versions to find the one working.
```bash
cd /home/plume/Plume
rustup override set nightly-2018-05-15 # this could be needed for compilation. If errors, try 2018-05-31.
# rustup override unset # remove the override for this directory.
```
* Rust nightly 2018-06-28 is known to be failing to compile diesel 1.3.2

View File

@ -1,87 +0,0 @@
# Installing Software Prerequisites
These instructions have been adapted from the Aardwolf documentation, and may not be accurate.
As such, this notification should be updated once verified for Plume installs.
> NOTE: These instructions may help in installing a production version, but are
intended for developers to be able to build and test their changes. If in doubt,
seek out documentation from your distribution package or from [the `doc` folder](doc).
## Installing Requirements
### Installing PostgreSQL
In order to run the Plume backend, you will need to have access to a
[PostgreSQL](https://www.postgresql.org/) database. There are a few options for doing this, but for
this guide were going to assume you are running the database on your
development machine.
#### Linux/OSX Instructions
If you're on an Ubuntu-like machine, you should be able to install
PostgreSQL like this:
$ sudo apt-get update
$ sudo apt-get install postgresql postgresql-contrib
If you see an error like:
= note: /usr/bin/ld: cannot find -lpq
collect2: error: ld returned 1 exit statusb
Then you may need to install the libpq (PostgreSQL C-library) package as well :
$ sudo apt-get install libpq-dev
If you're on OSX and using `brew`, do
$ brew update
$ brew install postgres
For Gentoo (eselect-postgresql is optional),
# emerge --sync
# emerge -av postgresql eselect-postgresql
For Fedora/CentOS/RHEL, do
# dnf install postgresql-server postgresql-contrib mariadb-devel libsq3-devel libpqxx libpqxx-devel
#### Windows Instructions
For Windows, just download the installer [here](https://www.enterprisedb.com/downloads/postgres-postgresql-downloads#windows) and run it. After installing, make sure to add the <POSTGRES INSTALL PATH>/lib directory to your PATH system variable.
### Installing rustup
> Note: Rustup managed installations do appear to co-exist with system
installations on Gentoo, and should work on most other distributions.
If not, please file an issue with the Rust and Rustup teams or your distributions
managers.
Next, youll need to have the [Rust](https://rust-lang.org/) toolchain
installed. The best way to do this is to install
[rustup](https://rustup.rs), which is a Rust toolchain manager.
#### Linux/OSX Instructions
Open your terminal and run the following command:
$ curl https://sh.rustup.rs -sSf | sh
For those who are (understandably) uncomfortable with piping a shell
script from the internet directly into `sh`, you can also
[use an alternate installation method](https://github.com/rust-lang-nursery/rustup.rs/#other-installation-methods).
#### Windows Instructions
If you don't already have them, download and install the [Visual C++ 2015 Build Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools).
Then, download the [rustup installer](https://www.rust-lang.org/en-US/install.html) and run it. That's it!
### Installing Rust Toolchain
Once you have `rustup` installed, make sure you have the `nightly` rust
toolchain installed:
$ rustup toolchain install nightly

View File

@ -1,74 +1,10 @@
# Development Guide
## Running Plume locally
## Installing the development environment
### Mac OSX
Please refer to the [installation guide](INSTALL.md).
All commands are run in the Mac Terminal or terminal emulator of your choice, such as iTerm2. First, you will need [Git](https://git-scm.com/download/mac), [Homebrew](https://brew.sh/), [Rust](https://www.rust-lang.org/en-US/), and [Postgres](https://www.postgresql.org/). Follow the instructions to install Homebrew before continuing if you don't already have it.
### Linux
Similar to Mac OSX all commands should be run from a terminal (a.k.a command line). First, you will need [Git](https://git-scm.com/download/mac), [Rust](https://www.rust-lang.org/en-US/), and [Postgres](https://www.postgresql.org/). Step-by-step instructions are also available here: [Installing Prerequisites](/doc/PREREQUISITES.md)
#### Download the Repository
Navigate to the directory on your machine where you would like to install the repository, such as in `~/dev` by running `cd dev`. Now, clone the remote repository by running `git clone https://github.com/Plume-org/Plume.git`. This will install the codebase to the `Plume` subdirectory. Navigate into that directory by running `cd Plume`.
#### Rust
If you think you might already have rust on your machine, you can check by running
```
rustc --version
# Should output something like
# rustc 1.28.0-nightly (a805a2a5e 2018-06-10)
```
If you don't already have Rust, install it by running
```
curl https://sh.rustup.rs -sSf | sh
```
In the interactive installation, choose the option of the nightly toolchain. Restart your console so that the `rustc` CLI tool is available.
#### Postgres
Now we will use Homebrew to install Postgres. If you think you might already have it, try running `brew info postgres`. If it is not available, continue to install Postgres by running the following:
```
brew install postgres
```
Now, you can use the following command to start Postgres on a one-time basis.
```
pg_ctl -D /usr/local/var/postgres start
```
When you will launch Plume for the first time, it will setup the database by itself.
#### Database Migration
To run migrations and correctly setup the database, Plume use the `diesel` CLI tool under the hood. Therefore you should install it before running Plume. If this was your time installing Rust, you will probably need to run that using `cargo`. `cargo` is installed with `rustc` so if you followed the earlier instructions it will already be available.
```
cargo install diesel_cli
```
#### Running Plume
To run Plume locally, make sure you are once again in the Plume directory, such as `~/dev/Plume`. Now you will be able to run the application using the command
```
cargo run
```
#### Configuration
The first time you'll run Plume, it will help you setup your instance through an interactive tool. Once you'll have answered all its question, your instance will start.
#### Testing the federation
## Testing the federation
To test the federation, you'll need to setup another database (see "Setup the database"),
also owned by the "plume" user, but with a different name. Then, you'll need to run the
@ -93,7 +29,7 @@ If you don't want to setup HTTPS locally, you can also disable it by running you
USE_HTTPS=0 cargo run
```
#### Making a Pull Request
## Making a Pull Request
To create an upstream fork of the repository in GitHub, click "Fork" in the top right button on the main page of the [Plume repository](https://github.com/Plume-org/Plume). Now, in the command line, set another remote for the repository by running the following command, replacing `myname` with the name under which you forked the repo. You can use another name besides `upstream` if you prefer. Using [SSH](https://help.github.com/articles/connecting-to-github-with-ssh/) is recommended.
```
@ -105,7 +41,7 @@ Now, make any changes to the code you want. After committing your changes, push
The project maintainers may suggest further changes to improve the pull request even more. After implementing this locally, you can push to your upstream fork again and the changes will immediately show up in the pull request after pushing. Once all the suggested changes are made, the pull request may be accepted. Thanks for contributing.
#### When working with Tera templates
## When working with Tera templates
When working with the interface, or any message that will be displayed to the final user, keep in mind that Plume is an internationalized software. To make sure that the parts of the interface you are changing are translatable, you should:

182
docs/INSTALL.md Normal file
View File

@ -0,0 +1,182 @@
# Installing Plume (for development or production)
## Prerequisites
In order to be installed and to work correctly, Plume needs:
- Git
- PostgreSQL
- GetText
- Rust and Cargo
All the following instructions will need a terminal.
Here are the commands to install PostgreSQL and GetText on various operating systems.
Some of them may need root permissions.
On **Debian**:
```bash
apt update
apt install gettext postgresql postgresql-contrib libpq-dev git
```
On **Fedora**, **CentOS** or **RHEL**:
```bash
dnf install postgresql-server postgresql-contrib mariadb-devel libsq3-devel libpqxx libpqxx-devel
# TODO: GetText + Git install
```
On **Gentoo**:
```bash
emerge --sync
emerge -av postgresql eselect-postgresql
# TODO: GetText + Git install
```
On **Mac OS X**, with [Homebrew](https://brew.sh/):
```bash
brew update
brew install postgres
# TODO: GetText + Git install
```
## Creating a new user (optional)
This step is recommended if you are in a **production environment**, but it is not necessary.
```bash
adduser plume
su - plume
cd ~
```
Creating a new user will let you use systemd to manage Plume if you want (see the dedicated section below).
## Installing Rust and Cargo
We said that Plume needed Rust and Cargo to work, but we didn't installed them at the same time as PostgreSQL and GetText, because there is an universal installation method called RustUp.
You can install it on **GNU/Linux** and **Mac OS X** with:
```bash
curl https://sh.rustup.rs -sSf | sh
```
On **Windows**, you'll need, if you don't already have them, to download and install the [Visual C++ 2015 Build Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools). Then, download the [rustup installer](https://www.rust-lang.org/en-US/install.html) and run it.
## Getting and compiling the Plume source code
Plume needs to be compiled from source.
```bash
git clone https://github.com/Plume-org/Plume.git
cd Plume
# This may take some time as RustUp will download all
# the required Rust components, and Cargo will download
# and compile all dependencies.
cargo build
```
We may provide precompiled packages and Docker images in the future (if you have experience in these fields and want to help, you're welcome).
## Configuring PostgreSQL
You can either run PostgreSQL from the machine that runs Plume, or from another server. We recommend you to use the first setup for development environments, or in production for small instances.
In the first case, just run this command after the PostgreSQL installation, to start it:
```
service postgresql start
```
If you want to have two separate machines, run these commands on the database server after you installed the dependencies mentionned above on both servers:
```bash
service postgresql start
su - postgres
createuser -d -P plume
createdb -O plume plume
```
```bash
```
## Running migrations
Migrations are scripts to update the database. They are run by a tool called Diesel, which can be installed with:
```bash
cargo install diesel_cli --no-default-features --features postgres --version '=1.2.0'
```
Plume should normally run migrations for you when needed, but if you want to run them manually, the command is:
```bash
diesel migration run --database-url postgres://USER:PASSWORD@IP:PORT/plume
```
This command may be useful if you decided to use a separate database server.
## Starting Plume
When you launch Plume for the first time, it will ask you a few questions to setup your instance before it actually launches. To start it, run these commands.
```
# Optional, only do it if the database URL is not
# postgres://plume:plume@localhost/plume
export DB_URL=postgres://plume:PASSWORD@DBSERVERIP:DBPORT/plume
cargo run
```
## Configuring Nginx
Here is a sample Nginx configuration for a Plume instance:
```nginx
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_pass http://localhost:7878;
client_max_body_size 16m;
}
```
## Systemd integration
If you want to manage your Plume instance with systemd, you can use the following unit file (to be saved in `/lib/systemd/system/plume.service`):
```toml
[Unit]
Description=plume
[Service]
Type=simple
User=plume
WorkingDirectory=/home/plume/Plume
ExecStart=/home/dev/.cargo/bin/cargo run
TimeoutSec=30
Restart=always
[Install]
WantedBy=multi-user.target
```
## Caveats:
- Pgbouncer is not yet supported (named transactions are used).
- Rust nightly is a moving target, dependancies can break and sometimes you need to check a few versions to find the one working (run `rustup override set nightly-2018-05-15` or `rustup override set nightly-2018-05-31` in the Plume directory if you have issues during the compilation)
- Rust nightly 2018-06-28 is known to be failing to compile diesel 1.3.2
## Acknowledgements
Most of this documentation have been written by *gled-rs*. The systemd unit file have been written by *nonbinaryanargeek*. Some parts (especially the instructions to install native dependencies) are from the [Aardwolf project](https://github.com/Aardwolf-Social/aardwolf).

5
docs/README.md Normal file
View File

@ -0,0 +1,5 @@
# Plume documentation
- [Installing Plume (for development or production)](INSTALL.md)
- [Development Guide](DEVELOPMENT.md)
- [Making Plume available in your language](INTERNATIONALIZATION.md)

1
docs/_config.yml Normal file
View File

@ -0,0 +1 @@
theme: jekyll-theme-cayman

View File

@ -0,0 +1,8 @@
-- This file should undo anything in `up.sql`
ALTER TABLE notifications ADD COLUMN title VARCHAR NOT NULL;
ALTER TABLE notifications ADD COLUMN content TEXT;
ALTER TABLE notifications ADD COLUMN link VARCHAR;
ALTER TABLE notifications ADD COLUMN data VARCHAR;
ALTER TABLE notifications DROP COLUMN kind;
ALTER TABLE notifications DROP COLUMN object_id;

View File

@ -0,0 +1,8 @@
-- Your SQL goes here
ALTER TABLE notifications DROP COLUMN title;
ALTER TABLE notifications DROP COLUMN content;
ALTER TABLE notifications DROP COLUMN link;
ALTER TABLE notifications DROP COLUMN data;
ALTER TABLE notifications ADD COLUMN kind VARCHAR NOT NULL DEFAULT 'unknown';
ALTER TABLE notifications ADD COLUMN object_id INTEGER NOT NULL DEFAULT 0;

View File

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

View File

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE users ADD COLUMN followers_endpoint VARCHAR NOT NULL DEFAULT '';

View File

@ -0,0 +1,5 @@
-- This file should undo anything in `up.sql`
ALTER TABLE instances DROP COLUMN open_registrations;
ALTER TABLE instances DROP COLUMN short_description;
ALTER TABLE instances DROP COLUMN long_description;
ALTER TABLE instances DROP COLUMN default_license;

View File

@ -0,0 +1,5 @@
-- Your SQL goes here
ALTER TABLE instances ADD COLUMN open_registrations BOOLEAN NOT NULL DEFAULT 't';
ALTER TABLE instances ADD COLUMN short_description TEXT NOT NULL DEFAULT '';
ALTER TABLE instances ADD COLUMN long_description TEXT NOT NULL DEFAULT '';
ALTER TABLE instances ADD COLUMN default_license TEXT NOT NULL DEFAULT 'CC-0';

View File

@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
ALTER TABLE instances DROP COLUMN long_description_html;
ALTER TABLE instances DROP COLUMN short_description_html;

View File

@ -0,0 +1,3 @@
-- Your SQL goes here
ALTER TABLE instances ADD COLUMN long_description_html VARCHAR NOT NULL DEFAULT '';
ALTER TABLE instances ADD COLUMN short_description_html VARCHAR NOT NULL DEFAULT '';

View File

@ -38,4 +38,6 @@ pub trait WithInbox {
fn get_inbox_url(&self) -> String;
fn get_shared_inbox_url(&self) -> Option<String>;
fn is_local(&self) -> bool;
}

View File

@ -2,9 +2,10 @@ use activitypub::{Activity, Actor, Object, Link};
use array_tool::vec::Uniq;
use reqwest::Client;
use rocket::{
Outcome,
http::Status,
response::{Response, Responder},
request::Request
request::{FromRequest, Request}
};
use serde_json;
@ -17,6 +18,15 @@ pub mod sign;
pub const CONTEXT_URL: &'static str = "https://www.w3.org/ns/activitystreams";
pub const PUBLIC_VISIBILTY: &'static str = "https://www.w3.org/ns/activitystreams#Public";
pub fn ap_accept_header() -> Vec<&'static str> {
vec![
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\"",
"application/ld+json;profile=\"https://w3.org/ns/activitystreams\"",
"application/activity+json",
"application/ld+json"
]
}
pub fn context() -> serde_json::Value {
json!([
CONTEXT_URL,
@ -59,8 +69,31 @@ impl<'r, O: Object> Responder<'r> for ActivityStream<O> {
}
}
#[derive(Clone)]
pub struct ApRequest;
impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
type Error = ();
fn from_request(request: &'a Request<'r>) -> Outcome<Self, (Status, Self::Error), ()> {
request.headers().get_one("Accept").map(|header| header.split(",").map(|ct| match ct.trim() {
// bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\"" |
"application/ld+json;profile=\"https://w3.org/ns/activitystreams\"" |
"application/activity+json" |
"application/ld+json" => Outcome::Success(ApRequest),
"text/html" => Outcome::Forward(true),
_ => Outcome::Forward(false)
}).fold(Outcome::Forward(false), |out, ct| if out.is_success() || (out.is_forward() && out.clone().forwarded().unwrap()) {
out
} else {
ct
}).map_forward(|_| ())).unwrap_or(Outcome::Forward(()))
}
}
pub fn broadcast<A: Activity, S: sign::Signer, T: inbox::WithInbox + Actor>(sender: &S, act: A, to: Vec<T>) {
let boxes = to.into_iter()
.filter(|u| !u.is_local())
.map(|u| u.get_shared_inbox_url().unwrap_or(u.get_inbox_url()))
.collect::<Vec<String>>()
.unique();

View File

@ -2,13 +2,11 @@ use base64;
use openssl::hash::{Hasher, MessageDigest};
use reqwest::{
mime::Mime,
header::{ContentType, Date, Headers, UserAgent}
};
use std::{
str::FromStr,
time::SystemTime
header::{Accept, Date, Headers, UserAgent, qitem}
};
use std::time::SystemTime;
use activity_pub::ap_accept_header;
use activity_pub::sign::Signer;
const USER_AGENT: &'static str = "Plume/0.1.0";
@ -25,7 +23,7 @@ pub fn headers() -> Headers {
let mut headers = Headers::new();
headers.set(UserAgent::new(USER_AGENT));
headers.set(Date(SystemTime::now().into()));
headers.set(ContentType(Mime::from_str("application/activity+json").unwrap()));
headers.set(Accept(ap_accept_header().into_iter().map(|h| qitem(h.parse::<Mime>().expect("Invalid Content-Type"))).collect()));
headers
}

View File

@ -28,10 +28,15 @@ pub fn md_to_html(md: &str) -> (String, Vec<String>) {
Event::Text(txt) => {
let (evts, _, _, _, new_mentions) = txt.chars().fold((vec![], false, String::new(), 0, vec![]), |(mut events, in_mention, text_acc, n, mut mentions), c| {
if in_mention {
if (c.is_alphanumeric() || c == '@' || c == '.' || c == '-' || c == '_') && (n < (txt.chars().count() - 1)) {
let char_matches = c.is_alphanumeric() || c == '@' || c == '.' || c == '-' || c == '_';
if char_matches && (n < (txt.chars().count() - 1)) {
(events, in_mention, text_acc + c.to_string().as_ref(), n + 1, mentions)
} else {
let mention = text_acc + c.to_string().as_ref();
let mention = if char_matches {
text_acc + c.to_string().as_ref()
} else {
text_acc
};
let short_mention = mention.clone();
let short_mention = short_mention.splitn(1, '@').nth(0).unwrap_or("");
let link = Tag::Link(format!("/@/{}/", mention).into(), short_mention.to_string().into());
@ -68,3 +73,27 @@ pub fn md_to_html(md: &str) -> (String, Vec<String>) {
html::push_html(&mut buf, parser);
(buf, mentions.collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mentions() {
let tests = vec![
("nothing", vec![]),
("@mention", vec!["mention"]),
("@mention@instance.tld", vec!["mention@instance.tld"]),
("@many @mentions", vec!["many", "mentions"]),
("@start with a mentions", vec!["start"]),
("mention at @end", vec!["end"]),
("between parenthesis (@test)", vec!["test"]),
("with some punctuation @test!", vec!["test"]),
(" @spaces ", vec!["spaces"]),
];
for (md, mentions) in tests {
assert_eq!(md_to_html(md).1, mentions.into_iter().map(|s| s.to_string()).collect::<Vec<String>>());
}
}
}

View File

@ -15,7 +15,7 @@ serde = "*"
serde_derive = "1.0"
serde_json = "1.0"
url = "1.7"
webfinger = "0.2"
webfinger = "0.3"
[dependencies.chrono]
features = ["serde"]

19
plume-models/src/admin.rs Normal file
View File

@ -0,0 +1,19 @@
use rocket::{Outcome, http::Status, request::{self, FromRequest, Request}};
use users::User;
/// Wrapper around User to use as a request guard on pages reserved to admins.
pub struct Admin(pub User);
impl<'a, 'r> FromRequest<'a, 'r> for Admin {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Admin, ()> {
let user = request.guard::<User>()?;
if user.is_admin {
Outcome::Success(Admin(user))
} else {
Outcome::Failure((Status::Unauthorized, ()))
}
}
}

View File

@ -18,11 +18,12 @@ use webfinger::*;
use {BASE_URL, USE_HTTPS};
use plume_common::activity_pub::{
ApSignature, ActivityStream, Id, IntoId, PublicKey,
ap_accept_header, ApSignature, ActivityStream, Id, IntoId, PublicKey,
inbox::WithInbox,
sign
};
use instance::*;
use users::User;
use schema::blogs;
pub type CustomGroup = CustomObject<ApSignature, Group>;
@ -68,6 +69,15 @@ impl Blog {
Instance::get(conn, self.instance_id).expect("Couldn't find instance")
}
pub fn list_authors(&self, conn: &PgConnection) -> Vec<User> {
use schema::blog_authors;
use schema::users;
let authors_ids = blog_authors::table.filter(blog_authors::blog_id.eq(self.id)).select(blog_authors::author_id);
users::table.filter(users::id.eq(any(authors_ids)))
.load::<User>(conn)
.expect("Couldn't load authors of a blog")
}
pub fn find_for_author(conn: &PgConnection, author_id: i32) -> Vec<Blog> {
use schema::blog_authors;
let author_ids = blog_authors::table.filter(blog_authors::author_id.eq(author_id)).select(blog_authors::blog_id);
@ -98,7 +108,7 @@ impl Blog {
fn fetch_from_webfinger(conn: &PgConnection, acct: String) -> Option<Blog> {
match resolve(acct.clone(), *USE_HTTPS) {
Ok(wf) => wf.links.into_iter().find(|l| l.mime_type == Some(String::from("application/activity+json"))).and_then(|l| Blog::fetch_from_url(conn, l.href)),
Ok(wf) => wf.links.into_iter().find(|l| l.mime_type == Some(String::from("application/activity+json"))).and_then(|l| Blog::fetch_from_url(conn, l.href.expect("No href for AP WF link"))),
Err(details) => {
println!("{:?}", details);
None
@ -109,7 +119,7 @@ impl Blog {
fn fetch_from_url(conn: &PgConnection, url: String) -> Option<Blog> {
let req = Client::new()
.get(&url[..])
.header(Accept(vec![qitem("application/activity+json".parse::<Mime>().unwrap())]))
.header(Accept(ap_accept_header().into_iter().map(|h| qitem(h.parse::<Mime>().expect("Invalid Content-Type"))).collect()))
.send();
match req {
Ok(mut res) => {
@ -130,7 +140,14 @@ impl Blog {
Instance::insert(conn, NewInstance {
public_domain: inst.clone(),
name: inst.clone(),
local: false
local: false,
// We don't really care about all the following for remote instances
long_description: String::new(),
short_description: String::new(),
default_license: String::new(),
open_registrations: true,
short_description_html: String::new(),
long_description_html: String::new()
})
}
};
@ -211,17 +228,20 @@ impl Blog {
Link {
rel: String::from("http://webfinger.net/rel/profile-page"),
mime_type: None,
href: self.ap_url.clone()
href: Some(self.ap_url.clone()),
template: None
},
Link {
rel: String::from("http://schemas.google.com/g/2010#updates-from"),
mime_type: Some(String::from("application/atom+xml")),
href: self.get_instance(conn).compute_box(BLOG_PREFIX, self.actor_id.clone(), "feed.atom")
href: Some(self.get_instance(conn).compute_box(BLOG_PREFIX, self.actor_id.clone(), "feed.atom")),
template: None
},
Link {
rel: String::from("self"),
mime_type: Some(String::from("application/activity+json")),
href: self.ap_url.clone()
href: Some(self.ap_url.clone()),
template: None
}
]
}
@ -238,6 +258,20 @@ impl Blog {
}
})
}
pub fn get_fqn(&self, conn: &PgConnection) -> String {
if self.instance_id == Instance::local_id(conn) {
self.actor_id.clone()
} else {
format!("{}@{}", self.actor_id, self.get_instance(conn).public_domain)
}
}
pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value {
let mut json = serde_json::to_value(self).unwrap();
json["fqn"] = json!(self.get_fqn(conn));
json
}
}
impl IntoId for Blog {
@ -257,6 +291,10 @@ impl WithInbox for Blog {
fn get_shared_inbox_url(&self) -> Option<String> {
None
}
fn is_local(&self) -> bool {
self.instance_id == 0
}
}
impl sign::Signer for Blog {

View File

@ -69,14 +69,17 @@ impl Comment {
.len()
}
pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value {
pub fn to_json(&self, conn: &PgConnection, others: &Vec<Comment>) -> serde_json::Value {
let mut json = serde_json::to_value(self).unwrap();
json["author"] = self.get_author(conn).to_json(conn);
let mentions = Mention::list_for_comment(conn, self.id).into_iter()
.map(|m| m.get_mentioned(conn).map(|u| u.get_fqn(conn)).unwrap_or(String::new()))
.collect::<Vec<String>>();
println!("{:?}", mentions);
json["mentions"] = serde_json::to_value(mentions).unwrap();
json["responses"] = json!(others.into_iter()
.filter(|c| c.in_response_to_id.map(|id| id == self.id).unwrap_or(false))
.map(|c| c.to_json(conn, others))
.collect::<Vec<_>>());
json
}
@ -120,10 +123,8 @@ impl Notify<PgConnection> for Comment {
fn notify(&self, conn: &PgConnection) {
for author in self.get_post(conn).get_authors(conn) {
Notification::insert(conn, NewNotification {
title: "{{ data }} commented your article".to_string(),
data: Some(self.get_author(conn).display_name.clone()),
content: Some(self.get_post(conn).title),
link: self.ap_url.clone(),
kind: notification_kind::COMMENT.to_string(),
object_id: self.id,
user_id: author.id
});
}

View File

@ -1,4 +1,4 @@
use activitypub::{Actor, activity::{Accept, Follow as FollowAct}};
use activitypub::{Actor, activity::{Accept, Follow as FollowAct}, actor::Person};
use diesel::{self, PgConnection, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{broadcast, Id, IntoId, inbox::{FromActivity, Notify, WithInbox}, sign::Signer};
@ -55,7 +55,9 @@ impl Follow {
impl FromActivity<FollowAct, PgConnection> for Follow {
fn from_activity(conn: &PgConnection, follow: FollowAct, _actor: Id) -> Follow {
let from = User::from_url(conn, follow.follow_props.actor.as_str().unwrap().to_string()).unwrap();
let from_id = follow.follow_props.actor_link::<Id>().map(|l| l.into())
.unwrap_or_else(|_| follow.follow_props.actor_object::<Person>().expect("No actor object (nor ID) on Follow").object_props.id_string().expect("No ID on actor on Follow"));
let from = User::from_url(conn, from_id).unwrap();
match User::from_url(conn, follow.follow_props.object.as_str().unwrap().to_string()) {
Some(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id),
None => {
@ -68,12 +70,9 @@ impl FromActivity<FollowAct, PgConnection> for Follow {
impl Notify<PgConnection> for Follow {
fn notify(&self, conn: &PgConnection) {
let follower = User::get(conn, self.follower_id).unwrap();
Notification::insert(conn, NewNotification {
title: "{{ data }} started following you".to_string(),
data: Some(follower.display_name.clone()),
content: None,
link: Some(follower.ap_url),
kind: notification_kind::FOLLOW.to_string(),
object_id: self.id,
user_id: self.following_id
});
}

View File

@ -2,6 +2,7 @@ use chrono::NaiveDateTime;
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods, PgConnection};
use std::iter::Iterator;
use plume_common::utils::md_to_html;
use ap_url;
use users::User;
use schema::{instances, users};
@ -13,7 +14,13 @@ pub struct Instance {
pub name: String,
pub local: bool,
pub blocked: bool,
pub creation_date: NaiveDateTime
pub creation_date: NaiveDateTime,
pub open_registrations: bool,
pub short_description: String,
pub long_description: String,
pub default_license : String,
pub long_description_html: String,
pub short_description_html: String
}
#[derive(Insertable)]
@ -21,7 +28,13 @@ pub struct Instance {
pub struct NewInstance {
pub public_domain: String,
pub name: String,
pub local: bool
pub local: bool,
pub open_registrations: bool,
pub short_description: String,
pub long_description: String,
pub default_license : String,
pub long_description_html: String,
pub short_description_html: String
}
impl Instance {
@ -68,4 +81,19 @@ impl Instance {
box_name = box_name
))
}
pub fn update(&self, conn: &PgConnection, name: String, open_registrations: bool, short_description: String, long_description: String) -> Instance {
let (sd, _) = md_to_html(short_description.as_ref());
let (ld, _) = md_to_html(long_description.as_ref());
diesel::update(self)
.set((
instances::name.eq(name),
instances::open_registrations.eq(open_registrations),
instances::short_description.eq(short_description),
instances::long_description.eq(long_description),
instances::short_description_html.eq(sd),
instances::long_description_html.eq(ld)
)).get_result::<Instance>(conn)
.expect("Couldn't update instance")
}
}

View File

@ -103,6 +103,7 @@ pub fn ap_url(url: String) -> String {
format!("{}://{}", scheme, url)
}
pub mod admin;
pub mod blog_authors;
pub mod blogs;
pub mod comments;

View File

@ -89,15 +89,11 @@ impl FromActivity<activity::Like, PgConnection> for Like {
impl Notify<PgConnection> for Like {
fn notify(&self, conn: &PgConnection) {
let liker = User::get(conn, self.user_id).unwrap();
let post = Post::get(conn, self.post_id).unwrap();
for author in post.get_authors(conn) {
let post = post.clone();
Notification::insert(conn, NewNotification {
title: "{{ data }} liked your article".to_string(),
data: Some(liker.display_name.clone()),
content: Some(post.title),
link: Some(post.ap_url),
kind: notification_kind::LIKE.to_string(),
object_id: self.id,
user_id: author.id
});
}

View File

@ -46,6 +46,13 @@ impl Mention {
self.comment_id.and_then(|id| Comment::get(conn, id))
}
pub fn get_user(&self, conn: &PgConnection) -> Option<User> {
match self.get_post(conn) {
Some(p) => p.get_authors(conn).into_iter().next(),
None => self.get_comment(conn).map(|c| c.get_author(conn))
}
}
pub fn build_activity(conn: &PgConnection, ment: String) -> link::Mention {
let user = User::find_by_fqn(conn, ment.clone());
let mut mention = link::Mention::default();
@ -63,8 +70,8 @@ impl Mention {
}
pub fn from_activity(conn: &PgConnection, ment: link::Mention, inside: i32, in_post: bool) -> Option<Self> {
let ap_url = ment.link_props.href_string().unwrap();
let mentioned = User::find_by_ap_url(conn, ap_url).unwrap();
let ap_url = ment.link_props.href_string().ok()?;
let mentioned = User::find_by_ap_url(conn, ap_url)?;
if in_post {
Post::get(conn, inside.clone().into()).map(|post| {
@ -94,16 +101,10 @@ impl Mention {
impl Notify<PgConnection> for Mention {
fn notify(&self, conn: &PgConnection) {
let author = self.get_comment(conn)
.map(|c| c.get_author(conn).display_name.clone())
.unwrap_or_else(|| self.get_post(conn).unwrap().get_authors(conn)[0].display_name.clone());
self.get_mentioned(conn).map(|m| {
Notification::insert(conn, NewNotification {
title: "{{ data }} mentioned you.".to_string(),
data: Some(author),
content: None,
link: Some(self.get_post(conn).map(|p| p.ap_url).unwrap_or_else(|| self.get_comment(conn).unwrap().ap_url.unwrap_or(String::new()))),
kind: notification_kind::MENTION.to_string(),
object_id: self.id,
user_id: m.id
});
});

View File

@ -1,28 +1,39 @@
use chrono::NaiveDateTime;
use diesel::{self, PgConnection, RunQueryDsl, QueryDsl, ExpressionMethods};
use serde_json;
use comments::Comment;
use follows::Follow;
use likes::Like;
use mentions::Mention;
use posts::Post;
use reshares::Reshare;
use users::User;
use schema::notifications;
pub mod notification_kind {
pub const COMMENT: &'static str = "COMMENT";
pub const FOLLOW: &'static str = "FOLLOW";
pub const LIKE: &'static str = "LIKE";
pub const MENTION: &'static str = "MENTION";
pub const RESHARE: &'static str = "RESHARE";
}
#[derive(Queryable, Identifiable, Serialize)]
pub struct Notification {
pub id: i32,
pub title: String,
pub content: Option<String>,
pub link: Option<String>,
pub user_id: i32,
pub creation_date: NaiveDateTime,
pub data: Option<String>
pub kind: String,
pub object_id: i32
}
#[derive(Insertable)]
#[table_name = "notifications"]
pub struct NewNotification {
pub title: String,
pub content: Option<String>,
pub link: Option<String>,
pub user_id: i32,
pub data: Option<String>
pub kind: String,
pub object_id: i32
}
impl Notification {
@ -35,4 +46,56 @@ impl Notification {
.load::<Notification>(conn)
.expect("Couldn't load user notifications")
}
pub fn page_for_user(conn: &PgConnection, user: &User, (min, max): (i32, i32)) -> Vec<Notification> {
notifications::table.filter(notifications::user_id.eq(user.id))
.order_by(notifications::creation_date.desc())
.offset(min.into())
.limit((max - min).into())
.load::<Notification>(conn)
.expect("Couldn't load user notifications page")
}
pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value {
let mut json = json!(self);
json["object"] = json!(match self.kind.as_ref() {
notification_kind::COMMENT => Comment::get(conn, self.object_id).map(|comment|
json!({
"post": comment.get_post(conn).to_json(conn),
"user": comment.get_author(conn).to_json(conn),
"id": comment.id
})
),
notification_kind::FOLLOW => Follow::get(conn, self.object_id).map(|follow|
json!({
"follower": User::get(conn, follow.follower_id).map(|u| u.to_json(conn))
})
),
notification_kind::LIKE => Like::get(conn, self.object_id).map(|like|
json!({
"post": Post::get(conn, like.post_id).map(|p| p.to_json(conn)),
"user": User::get(conn, like.user_id).map(|u| u.to_json(conn))
})
),
notification_kind::MENTION => Mention::get(conn, self.object_id).map(|mention|
json!({
"user": mention.get_user(conn).map(|u| u.to_json(conn)),
"url": mention.get_post(conn).map(|p| p.to_json(conn)["url"].clone())
.unwrap_or_else(|| {
let comment = mention.get_comment(conn).expect("No comment nor post for mention");
let post = comment.get_post(conn).to_json(conn);
json!(format!("{}#comment-{}", post["url"].as_str().unwrap(), comment.id))
})
})
),
notification_kind::RESHARE => Reshare::get(conn, self.object_id).map(|reshare|
json!({
"post": reshare.get_post(conn).map(|p| p.to_json(conn)),
"user": reshare.get_user(conn).map(|u| u.to_json(conn))
})
),
_ => Some(json!({}))
});
json
}
}

View File

@ -45,6 +45,7 @@ pub struct NewPost {
pub content: SafeString,
pub published: bool,
pub license: String,
pub creation_date: Option<NaiveDateTime>,
pub ap_url: String
}
@ -65,6 +66,10 @@ impl Post {
.len()
}
pub fn count(conn: &PgConnection) -> i64 {
posts::table.count().get_result(conn).expect("Couldn't count posts")
}
pub fn get_recents(conn: &PgConnection, limit: i64) -> Vec<Post> {
posts::table.order(posts::creation_date.desc())
.limit(limit)
@ -91,6 +96,29 @@ impl Post {
.expect("Error loading recent posts for blog")
}
pub fn get_for_blog(conn: &PgConnection, blog:&Blog) -> Vec<Post> {
posts::table.filter(posts::blog_id.eq(blog.id))
.load::<Post>(conn)
.expect("Error loading posts for blog")
}
pub fn blog_page(conn: &PgConnection, blog: &Blog, (min, max): (i32, i32)) -> Vec<Post> {
posts::table.filter(posts::blog_id.eq(blog.id))
.order(posts::creation_date.desc())
.offset(min.into())
.limit((max - min).into())
.load::<Post>(conn)
.expect("Error loading a page of posts for blog")
}
pub fn get_recents_page(conn: &PgConnection, (min, max): (i32, i32)) -> Vec<Post> {
posts::table.order(posts::creation_date.desc())
.offset(min.into())
.limit((max - min).into())
.load::<Post>(conn)
.expect("Error loading recent posts page")
}
pub fn get_authors(&self, conn: &PgConnection) -> Vec<User> {
use schema::users;
use schema::post_authors;
@ -151,7 +179,10 @@ impl Post {
let mut article = Article::default();
article.object_props.set_name_string(self.title.clone()).expect("Article::into_activity: name error");
article.object_props.set_id_string(self.ap_url.clone()).expect("Article::into_activity: id error");
article.object_props.set_attributed_to_link_vec::<Id>(self.get_authors(conn).into_iter().map(|x| Id::new(x.ap_url)).collect()).expect("Article::into_activity: attributedTo error");
let mut authors = self.get_authors(conn).into_iter().map(|x| Id::new(x.ap_url)).collect::<Vec<Id>>();
authors.push(self.get_blog(conn).into_id()); // add the blog URL here too
article.object_props.set_attributed_to_link_vec::<Id>(authors).expect("Article::into_activity: attributedTo error");
article.object_props.set_content_string(self.content.get().clone()).expect("Article::into_activity: content error");
article.object_props.set_published_utctime(Utc.from_utc_datetime(&self.creation_date)).expect("Article::into_activity: published error");
article.object_props.set_tag_link_vec(mentions).expect("Article::into_activity: tag error");
@ -175,63 +206,70 @@ impl Post {
}
pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value {
let blog = self.get_blog(conn);
json!({
"post": self,
"author": self.get_authors(conn)[0].to_json(conn),
"url": format!("/~/{}/{}/", self.get_blog(conn).actor_id, self.slug),
"date": self.creation_date.timestamp()
"url": format!("/~/{}/{}/", blog.get_fqn(conn), self.slug),
"date": self.creation_date.timestamp(),
"blog": blog.to_json(conn)
})
}
pub fn compute_id(&self, conn: &PgConnection) -> String {
ap_url(format!("{}/~/{}/{}/", BASE_URL.as_str(), self.get_blog(conn).actor_id, self.slug))
ap_url(format!("{}/~/{}/{}/", BASE_URL.as_str(), self.get_blog(conn).get_fqn(conn), self.slug))
}
}
impl FromActivity<Article, PgConnection> for Post {
fn from_activity(conn: &PgConnection, article: Article, _actor: Id) -> Post {
let (blog, authors) = article.object_props.attributed_to_link_vec::<Id>()
.expect("Post::from_activity: attributedTo error")
.into_iter()
.fold((None, vec![]), |(blog, mut authors), link| {
let url: String = link.into();
match User::from_url(conn, url.clone()) {
Some(user) => {
authors.push(user);
(blog, authors)
},
None => (blog.or_else(|| Blog::from_url(conn, url)), authors)
}
if let Some(post) = Post::find_by_ap_url(conn, article.object_props.id_string().unwrap_or(String::new())) {
post
} else {
let (blog, authors) = article.object_props.attributed_to_link_vec::<Id>()
.expect("Post::from_activity: attributedTo error")
.into_iter()
.fold((None, vec![]), |(blog, mut authors), link| {
let url: String = link.into();
match User::from_url(conn, url.clone()) {
Some(user) => {
authors.push(user);
(blog, authors)
},
None => (blog.or_else(|| Blog::from_url(conn, url)), authors)
}
});
let title = article.object_props.name_string().expect("Post::from_activity: title error");
let post = Post::insert(conn, NewPost {
blog_id: blog.expect("Received a new Article without a blog").id,
slug: title.to_kebab_case(),
title: title,
content: SafeString::new(&article.object_props.content_string().expect("Post::from_activity: content error")),
published: true,
license: String::from("CC-0"), // TODO
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url: article.object_props.url_string().unwrap_or(article.object_props.id_string().expect("Post::from_activity: url + id error")),
creation_date: Some(article.object_props.published_utctime().expect("Post::from_activity: published error").naive_utc())
});
let title = article.object_props.name_string().expect("Post::from_activity: title error");
let post = Post::insert(conn, NewPost {
blog_id: blog.expect("Received a new Article without a blog").id,
slug: title.to_kebab_case(),
title: title,
content: SafeString::new(&article.object_props.content_string().expect("Post::from_activity: content error")),
published: true,
license: String::from("CC-0"), // TODO
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url: article.object_props.url_string().unwrap_or(article.object_props.id_string().expect("Post::from_activity: url + id error"))
});
for author in authors.into_iter() {
PostAuthor::insert(conn, NewPostAuthor {
post_id: post.id,
author_id: author.id
});
}
// save mentions
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag.clone() {
for tag in tags.into_iter() {
serde_json::from_value::<link::Mention>(tag)
.map(|m| Mention::from_activity(conn, m, post.id, true))
.ok();
for author in authors.into_iter() {
PostAuthor::insert(conn, NewPostAuthor {
post_id: post.id,
author_id: author.id
});
}
// save mentions
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag.clone() {
for tag in tags.into_iter() {
serde_json::from_value::<link::Mention>(tag)
.map(|m| Mention::from_activity(conn, m, post.id, true))
.ok();
}
}
post
}
post
}
}

View File

@ -5,7 +5,7 @@ use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods};
use plume_common::activity_pub::{Id, IntoId, inbox::{FromActivity, Notify, Deletable}, PUBLIC_VISIBILTY};
use notifications::*;
use posts::Post;
use users::User;
use users::User;
use schema::reshares;
#[derive(Serialize, Deserialize, Queryable, Identifiable)]
@ -55,6 +55,10 @@ impl Reshare {
Post::get(conn, self.post_id)
}
pub fn get_user(&self, conn: &PgConnection) -> Option<User> {
User::get(conn, self.user_id)
}
pub fn delete(&self, conn: &PgConnection) -> Undo {
diesel::delete(self).execute(conn).unwrap();
@ -96,15 +100,11 @@ impl FromActivity<Announce, PgConnection> for Reshare {
impl Notify<PgConnection> for Reshare {
fn notify(&self, conn: &PgConnection) {
let actor = User::get(conn, self.user_id).unwrap();
let post = self.get_post(conn).unwrap();
for author in post.get_authors(conn) {
let post = post.clone();
Notification::insert(conn, NewNotification {
title: "{{ data }} reshared your article".to_string(),
data: Some(actor.display_name.clone()),
content: Some(post.title),
link: Some(post.ap_url),
kind: notification_kind::RESHARE.to_string(),
object_id: self.id,
user_id: author.id
});
}

View File

@ -53,6 +53,12 @@ table! {
local -> Bool,
blocked -> Bool,
creation_date -> Timestamp,
open_registrations -> Bool,
short_description -> Text,
long_description -> Text,
default_license -> Text,
long_description_html -> Varchar,
short_description_html -> Varchar,
}
}
@ -79,12 +85,10 @@ table! {
table! {
notifications (id) {
id -> Int4,
title -> Varchar,
content -> Nullable<Text>,
link -> Nullable<Varchar>,
user_id -> Int4,
creation_date -> Timestamp,
data -> Nullable<Varchar>,
kind -> Varchar,
object_id -> Int4,
}
}
@ -137,6 +141,7 @@ table! {
private_key -> Nullable<Text>,
public_key -> Text,
shared_inbox_url -> Nullable<Varchar>,
followers_endpoint -> Varchar,
}
}

View File

@ -1,5 +1,5 @@
use activitypub::{
Actor, Object, Endpoint, CustomObject,
Activity, Actor, Object, Endpoint, CustomObject,
actor::Person,
collection::OrderedCollection
};
@ -13,7 +13,7 @@ use openssl::{
sign
};
use plume_common::activity_pub::{
ActivityStream, Id, IntoId, ApSignature, PublicKey,
ap_accept_header, ActivityStream, Id, IntoId, ApSignature, PublicKey,
inbox::WithInbox,
sign::{Signer, gen_keypair}
};
@ -63,7 +63,8 @@ pub struct User {
pub ap_url: String,
pub private_key: Option<String>,
pub public_key: String,
pub shared_inbox_url: Option<String>
pub shared_inbox_url: Option<String>,
pub followers_endpoint: String
}
#[derive(Insertable)]
@ -81,7 +82,8 @@ pub struct NewUser {
pub ap_url: String,
pub private_key: Option<String>,
pub public_key: String,
pub shared_inbox_url: Option<String>
pub shared_inbox_url: Option<String>,
pub followers_endpoint: String
}
const USER_PREFIX: &'static str = "@";
@ -144,29 +146,32 @@ impl User {
fn fetch_from_webfinger(conn: &PgConnection, acct: String) -> Option<User> {
match resolve(acct.clone(), *USE_HTTPS) {
Ok(wf) => wf.links.into_iter().find(|l| l.mime_type == Some(String::from("application/activity+json"))).and_then(|l| User::fetch_from_url(conn, l.href)),
Ok(wf) => wf.links.into_iter().find(|l| l.mime_type == Some(String::from("application/activity+json"))).and_then(|l| User::fetch_from_url(conn, l.href.expect("No href for AP WF link"))),
Err(details) => {
println!("{:?}", details);
println!("WF Error: {:?}", details);
None
}
}
}
fn fetch_from_url(conn: &PgConnection, url: String) -> Option<User> {
pub fn fetch_from_url(conn: &PgConnection, url: String) -> Option<User> {
let req = Client::new()
.get(&url[..])
.header(Accept(vec![qitem("application/activity+json".parse::<Mime>().unwrap())]))
.header(Accept(ap_accept_header().into_iter().map(|h| qitem(h.parse::<Mime>().expect("Invalid Content-Type"))).collect()))
.send();
match req {
Ok(mut res) => {
let text = &res.text().unwrap();
let ap_sign: ApSignature = serde_json::from_str(text).unwrap();
let mut json: CustomPerson = serde_json::from_str(text).unwrap();
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()))
if let Ok(text) = &res.text() {
if let Ok(ap_sign) = serde_json::from_str::<ApSignature>(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
Some(User::from_activity(conn, json, Url::parse(url.as_ref()).unwrap().host_str().unwrap().to_string()))
} else { None }
} else { None }
} else { None }
},
Err(e) => {
println!("{:?}", e);
println!("User fetch error: {:?}", e);
None
}
}
@ -179,18 +184,24 @@ impl User {
Instance::insert(conn, NewInstance {
name: inst.clone(),
public_domain: inst.clone(),
local: false
local: false,
// We don't really care about all the following for remote instances
long_description: String::new(),
short_description: String::new(),
default_license: String::new(),
open_registrations: true,
short_description_html: String::new(),
long_description_html: String::new()
})
}
};
println!("User from act : {:?}", acct.custom_props);
User::insert(conn, NewUser {
username: acct.object.ap_actor_props.preferred_username_string().expect("User::from_activity: preferredUsername error"),
display_name: acct.object.object_props.name_string().expect("User::from_activity: name error"),
outbox_url: acct.object.ap_actor_props.outbox_string().expect("User::from_activity: outbox error"),
inbox_url: acct.object.ap_actor_props.inbox_string().expect("User::from_activity: inbox error"),
is_admin: false,
summary: SafeString::new(&acct.object.object_props.summary_string().expect("User::from_activity: summary error")),
summary: SafeString::new(&acct.object.object_props.summary_string().unwrap_or(String::new())),
email: None,
hashed_password: None,
instance_id: instance.id,
@ -199,7 +210,8 @@ impl User {
.public_key_pem_string().expect("User::from_activity: publicKey.publicKeyPem error"),
private_key: None,
shared_inbox_url: acct.object.ap_actor_props.endpoints_endpoint()
.and_then(|e| e.shared_inbox_string()).ok()
.and_then(|e| e.shared_inbox_string()).ok(),
followers_endpoint: acct.object.ap_actor_props.followers_string().expect("User::from_activity: followers error")
})
}
@ -240,6 +252,12 @@ impl User {
.set(users::shared_inbox_url.eq(ap_url(format!("{}/inbox", Instance::get_local(conn).unwrap().public_domain))))
.get_result::<User>(conn).expect("Couldn't update shared inbox URL");
}
if self.followers_endpoint.len() == 0 {
diesel::update(self)
.set(users::followers_endpoint.eq(instance.compute_box(USER_PREFIX, self.username.clone(), "followers")))
.get_result::<User>(conn).expect("Couldn't update followers endpoint");
}
}
pub fn outbox(&self, conn: &PgConnection) -> ActivityStream<OrderedCollection> {
@ -251,6 +269,50 @@ impl User {
ActivityStream::new(coll)
}
pub fn fetch_outbox<T: Activity>(&self) -> Vec<T> {
let req = Client::new()
.get(&self.outbox_url[..])
.header(Accept(ap_accept_header().into_iter().map(|h| qitem(h.parse::<Mime>().expect("Invalid Content-Type"))).collect()))
.send();
match req {
Ok(mut res) => {
let text = &res.text().unwrap();
let json: serde_json::Value = serde_json::from_str(text).unwrap();
json["items"].as_array()
.expect("Outbox.items is not an array")
.into_iter()
.filter_map(|j| serde_json::from_value(j.clone()).ok())
.collect::<Vec<T>>()
},
Err(e) => {
println!("User outbox fetch error: {:?}", e);
vec![]
}
}
}
pub fn fetch_followers_ids(&self) -> Vec<String> {
let req = Client::new()
.get(&self.followers_endpoint[..])
.header(Accept(ap_accept_header().into_iter().map(|h| qitem(h.parse::<Mime>().expect("Invalid Content-Type"))).collect()))
.send();
match req {
Ok(mut res) => {
let text = &res.text().unwrap();
let json: serde_json::Value = serde_json::from_str(text).unwrap();
json["items"].as_array()
.expect("Followers.items is not an array")
.into_iter()
.filter_map(|j| serde_json::from_value(j.clone()).ok())
.collect::<Vec<String>>()
},
Err(e) => {
println!("User followers fetch error: {:?}", e);
vec![]
}
}
}
fn get_activities(&self, conn: &PgConnection) -> Vec<serde_json::Value> {
use schema::posts;
use schema::post_authors;
@ -275,13 +337,22 @@ impl User {
users::table.filter(users::id.eq(any(follows))).load::<User>(conn).unwrap()
}
pub fn get_followers_page(&self, conn: &PgConnection, (min, max): (i32, i32)) -> Vec<User> {
use schema::follows;
let follows = Follow::belonging_to(self).select(follows::follower_id);
users::table.filter(users::id.eq(any(follows)))
.offset(min.into())
.limit((max - min).into())
.load::<User>(conn).unwrap()
}
pub fn get_following(&self, conn: &PgConnection) -> Vec<User> {
use schema::follows;
let follows = follows::table.filter(follows::follower_id.eq(self.id)).select(follows::following_id);
users::table.filter(users::id.eq(any(follows))).load::<User>(conn).unwrap()
}
pub fn is_following(&self, conn: &PgConnection, other_id: i32) -> bool {
pub fn is_followed_by(&self, conn: &PgConnection, other_id: i32) -> bool {
use schema::follows;
follows::table
.filter(follows::follower_id.eq(other_id))
@ -291,6 +362,16 @@ impl User {
.len() > 0
}
pub fn is_following(&self, conn: &PgConnection, other_id: i32) -> bool {
use schema::follows;
follows::table
.filter(follows::follower_id.eq(self.id))
.filter(follows::following_id.eq(other_id))
.load::<Follow>(conn)
.expect("Couldn't load follow relationship")
.len() > 0
}
pub fn has_liked(&self, conn: &PgConnection, post: &Post) -> bool {
use schema::likes;
likes::table
@ -333,6 +414,7 @@ impl User {
actor.ap_actor_props.set_inbox_string(self.inbox_url.clone()).expect("User::into_activity: inbox error");
actor.ap_actor_props.set_outbox_string(self.outbox_url.clone()).expect("User::into_activity: outbox error");
actor.ap_actor_props.set_preferred_username_string(self.username.clone()).expect("User::into_activity: preferredUsername error");
actor.ap_actor_props.set_followers_string(self.followers_endpoint.clone()).expect("User::into_activity: followers error");
let mut endpoints = Endpoint::default();
endpoints.set_shared_inbox_string(ap_url(format!("{}/inbox/", BASE_URL.as_str()))).expect("User::into_activity: endpoints.sharedInbox error");
@ -351,6 +433,11 @@ impl User {
pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value {
let mut json = serde_json::to_value(self).unwrap();
json["fqn"] = serde_json::Value::String(self.get_fqn(conn));
json["name"] = if self.display_name.len() > 0 {
json!(self.display_name)
} else {
json!(self.get_fqn(conn))
};
json
}
@ -362,17 +449,20 @@ impl User {
Link {
rel: String::from("http://webfinger.net/rel/profile-page"),
mime_type: None,
href: self.ap_url.clone()
href: Some(self.ap_url.clone()),
template: None
},
Link {
rel: String::from("http://schemas.google.com/g/2010#updates-from"),
mime_type: Some(String::from("application/atom+xml")),
href: self.get_instance(conn).compute_box(USER_PREFIX, self.username.clone(), "feed.atom")
href: Some(self.get_instance(conn).compute_box(USER_PREFIX, self.username.clone(), "feed.atom")),
template: None
},
Link {
rel: String::from("self"),
mime_type: Some(String::from("application/activity+json")),
href: self.ap_url.clone()
href: Some(self.ap_url.clone()),
template: None
}
]
}
@ -421,6 +511,10 @@ impl WithInbox for User {
fn get_shared_inbox_url(&self) -> Option<String> {
self.shared_inbox_url.clone()
}
fn is_local(&self) -> bool {
self.instance_id == 0
}
}
impl Signer for User {
@ -461,7 +555,8 @@ impl NewUser {
ap_url: String::from(""),
public_key: String::from_utf8(pub_key).unwrap(),
private_key: Some(String::from_utf8(priv_key).unwrap()),
shared_inbox_url: None
shared_inbox_url: None,
followers_endpoint: String::from("")
})
}
}

View File

@ -1,3 +1,6 @@
en
fr
pl
de
nb
gl

442
po/de.po Normal file
View File

@ -0,0 +1,442 @@
msgid ""
msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2018-08-01 13:50+0200\n"
"Last-Translator: cookie <git@bitkeks.eu>\n"
"Language-Team: none\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.0.9\n"
msgid "Latest articles"
msgstr "Neueste Artikel"
msgid "No posts to see here yet."
msgstr "Bisher keine Artikel vorhanden."
msgid "New article"
msgstr "Neuer Artikel"
msgid "New blog"
msgstr "Neuer Blog"
msgid "Create a blog"
msgstr "Erstelle einen Blog"
msgid "Title"
msgstr "Titel"
msgid "Create blog"
msgstr "Blog erstellen"
msgid "Comment \"{{ post }}\""
msgstr "Kommentar \"{{ post }}\""
msgid "Content"
msgstr "Inhalt"
msgid "Submit comment"
msgstr "Kommentar abschicken"
msgid "Something broke on our side."
msgstr "Bei dir ist etwas schief gegangen."
msgid "Sorry about that. If you think this is a bug, please report it."
msgstr ""
"Entschuldige. Wenn du denkst einen Bug gefunden zu haben, kannst du diesen "
"gerne melden."
msgid "Configuration"
msgstr "Konfiguration"
msgid "Configure your instance"
msgstr "Konfiguriere deine Instanz"
msgid "Name"
msgstr "Name"
msgid "Let&#x27;s go!"
msgstr "Los geht's!"
msgid "Welcome on {{ instance_name | escape }}"
msgstr "Willkommen auf {{ instance_name | escape }}"
msgid "Notifications"
msgstr "Benachrichtigungen"
msgid ""
"Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
msgstr ""
"Geschrieben von {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}"
"{{ link_3 }}"
msgid "This article is under the {{ license }} license."
msgstr "Dieser Artikel steht unter der {{ license }} Lizenz."
msgid "One like"
msgid_plural "{{ count }} likes"
msgstr[0] "Ein Like"
msgstr[1] "{{ count }} Likes"
msgid "I don&#x27;t like this anymore"
msgstr "Nicht mehr Liken"
msgid "Add yours"
msgstr "Like"
msgid "One reshare"
msgid_plural "{{ count }} reshares"
msgstr[0] "Ein Reshare"
msgstr[1] "{{ count }} Reshares"
msgid "I don&#x27;t want to reshare this anymore"
msgstr "Nicht mehr Resharen"
msgid "Reshare"
msgstr "Resharen"
msgid "Comments"
msgstr "Kommentare"
msgid "Respond"
msgstr "Antworten"
msgid "Comment"
msgstr "Kommentar"
msgid "New post"
msgstr "Neuer Beitrag"
msgid "Create a post"
msgstr "Beitrag erstellen"
msgid "Publish"
msgstr "Veröffentlichen"
msgid "Login"
msgstr "Login"
msgid "Username or email"
msgstr "Nutzername oder E-Mail"
msgid "Password"
msgstr "Passwort"
msgid "Dashboard"
msgstr "Dashboard"
msgid "Your Dashboard"
msgstr "Dein Dashboard"
msgid "Your Blogs"
msgstr "Deine Blogs"
msgid "You don&#x27;t have any blog yet. Create your own, or ask to join one."
msgstr ""
"Du hast bisher keinen Blog. Erstelle deinen eigenen oder tritt einem Blog "
"bei."
msgid "Start a new blog"
msgstr "Starte einen neuen Blog"
msgid "Admin"
msgstr "Admin"
msgid "It is you"
msgstr "Das bist du"
msgid "Edit your profile"
msgstr "Ändere dein Profil"
msgid "Open on {{ instance_url }}"
msgstr "Öffnen auf {{ instance_url }}"
msgid "Follow"
msgstr "Folgen"
msgid "Unfollow"
msgstr "Entfolgen"
msgid "Recently reshared"
msgstr "Vor Kurzem Reshared"
msgid "One follower"
msgid_plural "{{ count }} followers"
msgstr[0] "Ein Follower"
msgstr[1] "{{ count }} Followers"
msgid "Edit your account"
msgstr "Ändere deinen Account"
msgid "Your Profile"
msgstr "Dein Profil"
msgid "Display Name"
msgstr "Anzeigename"
msgid "Email"
msgstr "E-Mail"
msgid "Summary"
msgstr "Zusammenfassung"
msgid "Update account"
msgstr "Account aktualisieren"
msgid "{{ name | escape }}'s followers"
msgstr "{{ name | escape }}s Follower"
msgid "Followers"
msgstr "Follower"
msgid "New Account"
msgstr "Neuer Account"
msgid "Create an account"
msgstr "Erstelle einen Account"
msgid "Username"
msgstr "Nutzername"
msgid "Password confirmation"
msgstr "Passwort Wiederholung"
msgid "Create account"
msgstr "Account erstellen"
msgid "Plume"
msgstr "Plume"
msgid "Menu"
msgstr "Menü"
msgid "My account"
msgstr "Mein Account"
msgid "Log Out"
msgstr "Ausloggen"
msgid "Log In"
msgstr "Einloggen"
msgid "Register"
msgstr "Registrieren"
msgid "You need to be logged in order to create a new blog"
msgstr "Du musst eingeloggt sein, um einen neuen Blog zu erstellen"
msgid "You need to be logged in order to post a comment"
msgstr "Du musst eingeloggt sein, um einen Kommentar zu schreiben"
msgid "You need to be logged in order to like a post"
msgstr "Du musst eingeloggt sein, um einen Beitrag zu Liken"
msgid "You need to be logged in order to see your notifications"
msgstr "Du musst eingeloggt sein, um deine Benachrichtigungen zu sehen"
msgid "You need to be logged in order to write a new post"
msgstr "Du musst eingeloggt sein, um einen neuen Beitrag zu schreiben"
msgid "You need to be logged in order to reshare a post"
msgstr "Du musst eingeloggt sein, um einen Beitrag zu resharen"
msgid "Invalid username or password"
msgstr "Nutzername oder Passwort ungültig"
msgid "You need to be logged in order to access your dashboard"
msgstr "Du musst eingeloggt sein, um dein Dashboard zu sehen"
msgid "You need to be logged in order to follow someone"
msgstr "Du musst eingeloggt sein, um jemandem zu folgen"
msgid "You need to be logged in order to edit your profile"
msgstr "Du musst eingeloggt sein, um dein Profil zu editieren"
msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}"
msgstr ""
"Von {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}"
msgid "{{ data }} reshared your article"
msgstr "{{ data }} hat deinen Artikel reshared"
msgid "{{ data }} started following you"
msgstr "{{ data }} folgt dir nun"
msgid "{{ data }} liked your article"
msgstr "{{ data }} hat deinen Artikel geliked"
msgid "{{ data }} commented your article"
msgstr "{{ data }} hat deinen Artikel kommentiert"
msgid "We couldn&#x27;t find this page."
msgstr "Wir konnten diese Seite nicht finden."
msgid "The link that led you here may be broken."
msgstr "Der Link, welcher dich hier her führte, ist wohl kaputt."
msgid "You are not authorized."
msgstr "Nicht berechtigt."
msgid "You are not author in this blog."
msgstr "Du bist kein Autor in diesem Blog."
msgid "{{ data }} mentioned you."
msgstr "{{ data }} hat dich erwähnt."
msgid "Your comment"
msgstr "Dein Kommentar"
msgid "Unknown error"
msgstr "Unbekannter Fehler"
msgid "Invalid name"
msgstr "Ungültiger Name"
msgid "A blog with the same name already exists."
msgstr "Ein Blog mit demselben Namen existiert bereits."
msgid "Your comment can't be empty"
msgstr "Dein Kommentar kann nicht leer sein"
msgid "A post with the same title already exists."
msgstr "Ein Beitrag mit demselben Titel existiert bereits."
msgid "We need an email or a username to identify you"
msgstr ""
"Wir brauchen eine E-Mail oder einen Nutzernamen um dich zu identifizieren"
#, fuzzy
msgid "Your password can't be empty"
msgstr "Dein Kommentar kann nicht leer sein"
msgid "Passwords are not matching"
msgstr "Passwörter stimmen nicht überein"
msgid "Username can't be empty"
msgstr "Nutzername kann nicht leer sein"
msgid "Invalid email"
msgstr "Ungültige E-Mail"
msgid "Password should be at least 8 characters long"
msgstr "Passwort sollte mindestens 8 Zeichen lang sein"
msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: "
msgstr[0] "Ein Autor in diesem Blog: "
msgstr[1] "{{ count }} Autoren in diesem Blog: "
msgid "Login or use your Fediverse account to interact with this article"
msgstr ""
"Log dich ein oder nutze deinen Fediverse-Account um mit diesem Artikel zu "
"interagieren"
msgid "Optional"
msgstr "Optional"
msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog"
msgstr[0] "Ein Artikel in diesem Blog"
msgstr[1] "{{ count }} Artikel in diesem Blog"
msgid "Previous page"
msgstr "Vorherige Seite"
msgid "Next page"
msgstr "Nächste Seite"
msgid "{{ user }} mentioned you."
msgstr "{{ user }} hat dich erwähnt."
msgid "{{ user }} commented your article."
msgstr "{{ user }} hat deinen Artikel kommentiert."
msgid "{{ user }} is now following you."
msgstr "{{ user }} folgt dir nun."
msgid "{{ user }} liked your article."
msgstr "{{ user }} hat deinen Artikel geliked."
msgid "{{ user }} reshared your article."
msgstr "{{ user }} hat deinen Artikel reshared."
msgid "Source code"
msgstr "Quellcode"
msgid "Matrix room"
msgstr "Matrix-Raum"
msgid "Administration"
msgstr "Administration"
msgid "Instance settings"
msgstr "Instanz-Einstellungen"
msgid "Allow anyone to register"
msgstr "Erlaube jedem die Registrierung"
msgid "Short description"
msgstr "Kurze Beschreibung"
msgid "Markdown is supported"
msgstr "Markdown wird unterstützt"
msgid "Long description"
msgstr "Lange Beschreibung"
msgid "Default license"
msgstr "Voreingestellte Lizenz"
msgid "Save settings"
msgstr "Einstellungen speichern"
msgid "No comments yet. Be the first to react!"
msgstr "Bisher keine Kommentare. Schreibe die erste Reaktion!"
msgid "About this instance"
msgstr "Über diese Instanz"
msgid "What is Plume?"
msgstr "Was ist Plume?"
msgid "Plume is a decentralized blogging engine."
msgstr "Plume ist eine dezentrale Blogging-Engine."
msgid "Authors can manage various blogs from an unique website."
msgstr "Autoren können verschiedene Blogs von einer Website aus verwalten."
msgid ""
"Articles are also visible on other Plume websites, and you can interact with "
"them directly from other platforms like Mastodon."
msgstr ""
"Artikel sind auch auf anderen Plume-Websites sichtbar und es ist möglich aus "
"anderen Plattformen wie Mastodon mit diesen zu interagieren."
msgid "Create your account"
msgstr "Eigenen Account erstellen"
msgid "About {{ instance_name }}"
msgstr "Über {{ instance_name }}"
msgid "Home to"
msgstr "Heimat von"
msgid "people"
msgstr "Menschen"
msgid "Who wrote"
msgstr "Die"
msgid "articles"
msgstr "Artikel geschrieben haben"
msgid "Read the detailed rules"
msgstr "Lies die detailierten Regeln"
#~ msgid "Your password should be at least 8 characters long"
#~ msgstr "Das Passwort sollte mindestens 8 Zeichen lang sein"

142
po/en.po
View File

@ -288,3 +288,145 @@ msgstr ""
msgid "Your comment"
msgstr ""
msgid "Unknown error"
msgstr ""
msgid "Invalid name"
msgstr ""
msgid "A blog with the same name already exists."
msgstr ""
msgid "Your comment can't be empty"
msgstr ""
msgid "A post with the same title already exists."
msgstr ""
msgid "We need an email or a username to identify you"
msgstr ""
msgid "Your password can't be empty"
msgstr ""
msgid "Passwords are not matching"
msgstr ""
msgid "Username can't be empty"
msgstr ""
msgid "Invalid email"
msgstr ""
msgid "Password should be at least 8 characters long"
msgstr ""
msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: "
msgstr[0] ""
msgstr[1] ""
msgid "Login or use your Fediverse account to interact with this article"
msgstr ""
msgid "Optional"
msgstr ""
msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog"
msgstr[0] ""
msgstr[1] ""
msgid "Previous page"
msgstr ""
msgid "Next page"
msgstr ""
msgid "{{ user }} mentioned you."
msgstr ""
msgid "{{ user }} commented your article."
msgstr ""
msgid "{{ user }} is now following you."
msgstr ""
msgid "{{ user }} liked your article."
msgstr ""
msgid "{{ user }} reshared your article."
msgstr ""
msgid "Source code"
msgstr ""
msgid "Matrix room"
msgstr ""
msgid "Administration"
msgstr ""
msgid "Instance settings"
msgstr ""
msgid "Allow anyone to register"
msgstr ""
msgid "Short description"
msgstr ""
msgid "Markdown is supported"
msgstr ""
msgid "Long description"
msgstr ""
msgid "Default license"
msgstr ""
msgid "Save settings"
msgstr ""
msgid "No comments yet. Be the first to react!"
msgstr ""
msgid "About this instance"
msgstr ""
msgid "What is Plume?"
msgstr ""
msgid "Plume is a decentralized blogging engine."
msgstr ""
msgid "Authors can manage various blogs from an unique website."
msgstr ""
msgid ""
"Articles are also visible on other Plume websites, and you can interact with "
"them directly from other platforms like Mastodon."
msgstr ""
msgid "Create your account"
msgstr ""
#, fuzzy
msgid "About {{ instance_name }}"
msgstr "Welcome on {{ instance_name }}"
msgid "Home to"
msgstr ""
msgid "people"
msgstr ""
msgid "Who wrote"
msgstr ""
msgid "articles"
msgstr ""
msgid "Read the detailed rules"
msgstr ""

192
po/fr.po
View File

@ -43,18 +43,18 @@ msgid "Submit comment"
msgstr "Envoyer le commentaire"
msgid "Something broke on our side."
msgstr "Nous avons cassé quelque chose"
msgstr "Nous avons cassé quelque chose."
msgid "Sorry about that. If you think this is a bug, please report it."
msgstr ""
"Nous sommes désolé⋅e⋅s. Si vous pensez que c'est un bogue, merci de le "
"rapporter."
"signaler."
msgid "Configuration"
msgstr "Configuration"
msgid "Configure your instance"
msgstr "Configurez votre instance"
msgstr "Configurer votre instance"
msgid "Name"
msgstr "Nom"
@ -62,17 +62,16 @@ msgstr "Nom"
msgid "Let&#x27;s go!"
msgstr "C'est parti !"
#, fuzzy
msgid "Welcome on {{ instance_name | escape }}"
msgstr "Bienvenue sur {{ instance_name }}"
msgstr "Bienvenue sur {{ instance_name | escape }}"
msgid "Notifications"
msgstr "Notifications"
#, fuzzy
msgid ""
"Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
msgstr "Écrit par {{ link_1 }}{{ url }}{{ link_2 }}{{ name }}{{ link_3 }}"
msgstr ""
"Écrit par {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
msgid "This article is under the {{ license }} license."
msgstr "Cet article est placé sous la licence {{ license }}"
@ -94,10 +93,10 @@ msgstr[0] "{{ count }} repartage"
msgstr[1] "{{ count }} repartages"
msgid "I don&#x27;t want to reshare this anymore"
msgstr "Je ne veux plus repartager"
msgstr "Je ne veux plus repartager ceci"
msgid "Reshare"
msgstr "Repartagez"
msgstr "Repartager"
msgid "Comments"
msgstr "Commentaires"
@ -106,7 +105,7 @@ msgid "Respond"
msgstr "Répondre"
msgid "Comment"
msgstr "Commentez"
msgstr "Commenter"
msgid "New post"
msgstr "Nouvel article"
@ -141,7 +140,7 @@ msgstr ""
"un."
msgid "Start a new blog"
msgstr "Commencez un nouveau blog"
msgstr "Commencer un nouveau blog"
msgid "Admin"
msgstr "Administrateur"
@ -150,10 +149,10 @@ msgid "It is you"
msgstr "C'est vous"
msgid "Edit your profile"
msgstr "Éditez votre profil"
msgstr "Éditer votre profil"
msgid "Open on {{ instance_url }}"
msgstr "Bienvenue sur {{ instance_name }}"
msgstr "Ouvrir sur {{ instance_url }}"
msgid "Follow"
msgstr "S'abonner"
@ -187,12 +186,11 @@ msgstr "Description"
msgid "Update account"
msgstr "Mettre à jour mes informations"
#, fuzzy
msgid "{{ name | escape }}'s followers"
msgstr "{{ count }} abonné⋅e"
msgstr "Les abonné⋅e⋅s de {{ name | escape }}"
msgid "Followers"
msgstr "{{ count }} abonné⋅e"
msgstr "Abonné⋅e⋅s"
msgid "New Account"
msgstr "Nouveau compte"
@ -257,9 +255,8 @@ msgstr "Vous devez vous connecter pour suivre quelqu'un"
msgid "You need to be logged in order to edit your profile"
msgstr "Vous devez vous connecter pour modifier votre profil"
#, fuzzy
msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}"
msgstr "De {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name }}{{ link_4 }}"
msgstr "Par {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}"
msgid "{{ data }} reshared your article"
msgstr "{{ data }} a repartagé votre article"
@ -277,17 +274,168 @@ msgid "We couldn&#x27;t find this page."
msgstr "Page introuvable."
msgid "The link that led you here may be broken."
msgstr "Le lien que vous avez suivi est cassé."
msgstr "Vous avez probablement suivi un lien cassé."
msgid "You are not authorized."
msgstr "Vous n'avez pas les droits."
msgid "You are not author in this blog."
msgstr "Vous n'êtes pas auteur dans ce blog."
msgstr "Vous n'êtes pas auteur⋅ice dans ce blog."
msgid "{{ data }} mentioned you."
msgstr "{{ data }} vous a mentionné."
msgid "Your comment"
msgstr "Votre commentaire"
msgid "Unknown error"
msgstr "Erreur inconnue"
msgid "Invalid name"
msgstr "Nom invalide"
msgid "A blog with the same name already exists."
msgstr "Un blog avec le même nom existe déjà."
msgid "Your comment can't be empty"
msgstr "Votre commentaire ne peut pas être vide."
msgid "A post with the same title already exists."
msgstr "Un article avec le même titre existe déjà."
msgid "We need an email or a username to identify you"
msgstr ""
"Nous avons besoin d'une adresse mail ou d'un nom d'utilisateur pour vous "
"identifier."
#, fuzzy
msgid "Your comment"
msgstr "Envoyer le commentaire"
msgid "Your password can't be empty"
msgstr "Votre mot de passe ne peut pas être vide."
msgid "Passwords are not matching"
msgstr "Les mots de passe ne correspondent pas."
msgid "Username can't be empty"
msgstr "Le nom d'utilisateur ne peut pas être vide."
msgid "Invalid email"
msgstr "Adresse mail invalide."
msgid "Password should be at least 8 characters long"
msgstr "Le mot de passe doit faire au moins 8 caractères."
msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: "
msgstr[0] "{{ count }} auteur⋅ice dans ce blog : "
msgstr[1] "{{ count }} auteur⋅ice⋅s dans ce blog : "
msgid "Login or use your Fediverse account to interact with this article"
msgstr ""
"Connectez-vous ou utilisez votre compte sur le Fediverse pour interagir avec "
"cet article"
msgid "Optional"
msgstr "Optionnel"
#, fuzzy
msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog"
msgstr[0] "{{ count }} article dans ce blog"
msgstr[1] "{{ count }} articles dans ce blog"
msgid "Previous page"
msgstr "Page précédente"
msgid "Next page"
msgstr "Page suivante"
msgid "{{ user }} mentioned you."
msgstr "{{ user }} vous a mentionné."
msgid "{{ user }} commented your article."
msgstr "{{ user }} a commenté votre article"
msgid "{{ user }} is now following you."
msgstr "{{ user }} vous suit"
msgid "{{ user }} liked your article."
msgstr "{{ user }} a aimé votre article"
msgid "{{ user }} reshared your article."
msgstr "{{ user }} a repartagé votre article"
msgid "Source code"
msgstr "Code source"
msgid "Matrix room"
msgstr "Salon Matrix"
msgid "Administration"
msgstr "Administration"
msgid "Instance settings"
msgstr "Paramètres de l'instance"
msgid "Allow anyone to register"
msgstr "Autoriser les inscriptions"
msgid "Short description"
msgstr "Description courte"
msgid "Markdown is supported"
msgstr "Vous pouvez utiliser du Markdown"
msgid "Long description"
msgstr "Description longue"
msgid "Default license"
msgstr "License par défaut"
msgid "Save settings"
msgstr "Enregistrer les paramètres"
msgid "No comments yet. Be the first to react!"
msgstr "Pas encore de commentaires. Soyez læ premier⋅ère à réagir !"
msgid "About this instance"
msgstr "À propos de cette instance"
msgid "What is Plume?"
msgstr "Qu'est-ce que Plume ?"
msgid "Plume is a decentralized blogging engine."
msgstr "Plume est un moteur de blog décentralisé."
msgid "Authors can manage various blogs from an unique website."
msgstr "Les auteur⋅ice⋅s peuvent gérer différents blogs au sein d'un même site."
msgid ""
"Articles are also visible on other Plume websites, and you can interact with "
"them directly from other platforms like Mastodon."
msgstr ""
"Les articles sont également visibles sur d'autres sites Plume, et vous pouvez "
"interagir avec directement depuis d'autres plateformes telles que Mastodon."
msgid "Create your account"
msgstr "Créer votre compte"
msgid "About {{ instance_name }}"
msgstr "À propos de {{ instance_name }}"
msgid "Home to"
msgstr "Accueille"
msgid "people"
msgstr "personnes"
msgid "Who wrote"
msgstr "Ayant écrit"
msgid "articles"
msgstr "articles"
msgid "Read the detailed rules"
msgstr "Lire les règles détaillées"
#~ msgid "Your password should be at least 8 characters long"
#~ msgstr "Votre mot de passe doit faire au moins 8 caractères."

426
po/gl.po Normal file
View File

@ -0,0 +1,426 @@
msgid ""
msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2018-06-15 16:33-0700\n"
"Last-Translator: Xosé M. <correo@xmgz.eu>\n"
"Language-Team: none\n"
"Language: gl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
msgid "Latest articles"
msgstr "Últimos artigos"
msgid "No posts to see here yet."
msgstr "Aínda non hai entradas publicadas"
msgid "New article"
msgstr "Novo artigo"
msgid "New blog"
msgstr "Novo blog"
msgid "Create a blog"
msgstr "Crear un blog"
msgid "Title"
msgstr "Título"
msgid "Create blog"
msgstr "Crear blog"
msgid "Comment \"{{ post }}\""
msgstr "Comentar \"{{ post }}\""
msgid "Content"
msgstr "Contido"
msgid "Submit comment"
msgstr "Enviar comentario"
msgid "Something broke on our side."
msgstr "Algo fallou pola nosa parte"
msgid "Sorry about that. If you think this is a bug, please report it."
msgstr "Lamentálmolo. Si cree que é un bug, infórmenos por favor."
msgid "Configuration"
msgstr "Axustes"
msgid "Configure your instance"
msgstr "Configure a súa instancia"
msgid "Name"
msgstr "Nome"
msgid "Let&#x27;s go!"
msgstr "Imos!"
msgid "Welcome on {{ instance_name | escape }}"
msgstr "Ben vida a {{ instance_name | escape }}"
msgid "Notifications"
msgstr "Notificacións"
msgid ""
"Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
msgstr "Escrito por {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
msgid "This article is under the {{ license }} license."
msgstr "Este artigo ten licenza {{ license }}"
msgid "One like"
msgid_plural "{{ count }} likes"
msgstr[0] "Un gústame"
msgstr[1] "{{ count }} gústame"
msgid "I don&#x27;t like this anymore"
msgstr "Xa non me gusta"
msgid "Add yours"
msgstr "Engada os seus"
msgid "One reshare"
msgid_plural "{{ count }} reshares"
msgstr[0] "Compartido unha vez"
msgstr[1] "Compartido {{ count }} veces"
msgid "I don&#x27;t want to reshare this anymore"
msgstr "Xa non quero compartir esto"
msgid "Reshare"
msgstr "Compartir"
msgid "Comments"
msgstr "Comentarios"
msgid "Respond"
msgstr "Respostar"
msgid "Comment"
msgstr "Comentar"
msgid "New post"
msgstr "Nova entrada"
msgid "Create a post"
msgstr "Crear unha entrada"
msgid "Publish"
msgstr "Publicar"
msgid "Login"
msgstr "Conectar"
msgid "Username or email"
msgstr "Usuaria ou correo-e"
msgid "Password"
msgstr "Contrasinal"
msgid "Dashboard"
msgstr "Taboleiro"
msgid "Your Dashboard"
msgstr "O seu taboleiro"
msgid "Your Blogs"
msgstr "Os seus Blogs"
msgid "You don&#x27;t have any blog yet. Create your own, or ask to join one."
msgstr "Aínda non ten blogs. Publique un, ou solicita unirse a un."
msgid "Start a new blog"
msgstr "Iniciar un blog"
msgid "Admin"
msgstr "Admin"
msgid "It is you"
msgstr "É vosted"
msgid "Edit your profile"
msgstr "Edite o seu perfil"
msgid "Open on {{ instance_url }}"
msgstr "Abrir en {{ instance_url }}"
msgid "Follow"
msgstr "Seguir"
msgid "Unfollow"
msgstr "Deixar de seguir"
msgid "Recently reshared"
msgstr "Compartido recentemente"
msgid "One follower"
msgid_plural "{{ count }} followers"
msgstr[0] "Unha seguidora"
msgstr[1] "{{ count }} seguidoras"
msgid "Edit your account"
msgstr "Edite a súa conta"
msgid "Your Profile"
msgstr "O seu perfil"
msgid "Display Name"
msgstr "Nome mostrado"
msgid "Email"
msgstr "Correo-e"
msgid "Summary"
msgstr "Resumen"
msgid "Update account"
msgstr "Actualizar conta"
msgid "{{ name | escape }}'s followers"
msgstr "Seguidoras de {{ name | escape }}"
msgid "Followers"
msgstr "Seguidoras"
msgid "New Account"
msgstr "Nova conta"
msgid "Create an account"
msgstr "Crear unha conta"
msgid "Username"
msgstr "Nome de usuaria"
msgid "Password confirmation"
msgstr "Confirmación do contrasinal"
msgid "Create account"
msgstr "Crear conta"
msgid "Plume"
msgstr "Plume"
msgid "Menu"
msgstr "Menú"
msgid "My account"
msgstr "A miña conta"
msgid "Log Out"
msgstr "Desconectar"
msgid "Log In"
msgstr "Conectar"
msgid "Register"
msgstr "Rexistrar"
msgid "You need to be logged in order to create a new blog"
msgstr "Debe estar conectada para crear un novo blog"
msgid "You need to be logged in order to post a comment"
msgstr "Debe estar conectada para publicar un comentario"
msgid "You need to be logged in order to like a post"
msgstr "Debe estar conectada para gustar unha entrada"
msgid "You need to be logged in order to see your notifications"
msgstr "Debe estar conectada para ver as súas notificacións"
msgid "You need to be logged in order to write a new post"
msgstr "Debe estar conectada para escribir unha nova entrada"
msgid "You need to be logged in order to reshare a post"
msgstr "Debe estar conectada para compartir unha entrada"
msgid "Invalid username or password"
msgstr "Usuaria ou Contrasinal incorrectos"
msgid "You need to be logged in order to access your dashboard"
msgstr "Debe estar conectada para acceder ao seu taboleiro"
msgid "You need to be logged in order to follow someone"
msgstr "Debe estar conectada para seguir a alguén"
msgid "You need to be logged in order to edit your profile"
msgstr "Debe estar conectada para editar o seu perfil"
msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}"
msgstr "Por {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}"
msgid "{{ data }} reshared your article"
msgstr "{{ data }} comparteu o seu artigo"
msgid "{{ data }} started following you"
msgstr "{{ data }} comezou a seguila"
msgid "{{ data }} liked your article"
msgstr "{{ data }} gustou do seu artigo"
msgid "{{ data }} commented your article"
msgstr "{{ data }} comentou o seu artigo"
msgid "We couldn&#x27;t find this page."
msgstr "Non atopamos esta páxina"
msgid "The link that led you here may be broken."
msgstr "A ligazón que a trouxo aquí podería estar quebrado"
msgid "You are not authorized."
msgstr "Non ten permiso."
msgid "You are not author in this blog."
msgstr "Vostede non é autora en este blog."
msgid "{{ data }} mentioned you."
msgstr "{{ data }} mencionouna."
msgid "Your comment"
msgstr "O seu comentario"
msgid "Unknown error"
msgstr "Fallo descoñecido"
msgid "Invalid name"
msgstr "Nome non válido"
msgid "A blog with the same name already exists."
msgstr "Xa existe un blog co mismo nome."
msgid "Your comment can't be empty"
msgstr "O seu comentario non pode estar baldeiro"
msgid "A post with the same title already exists."
msgstr "Xa existe unha entrada co mismo nome."
msgid "We need an email or a username to identify you"
msgstr "Precisamos un correo-e ou un nome de usuaria para identificala"
msgid "Your password can't be empty"
msgstr "O contrasinal non pode estar baldeiro"
msgid "Passwords are not matching"
msgstr "Con coinciden os contrasinais"
msgid "Username can't be empty"
msgstr "O nome de usuaria non pode estar baldeiro"
msgid "Invalid email"
msgstr "Correo-e non válido"
msgid "Password should be at least 8 characters long"
msgstr "O contrasinal debe ter ao menos 8 caracteres"
msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: "
msgstr[0] "Unha autora en este blog: "
msgstr[1] "{{ count }} autoras en este blog: "
msgid "Login or use your Fediverse account to interact with this article"
msgstr "Conéctese ou utilice a súa conta no fediverso para interactuar con este artigo"
msgid "Optional"
msgstr "Opcional"
msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog"
msgstr[0] "Un artigo en este blog"
msgstr[1] "{{ count }} artigos en este blog"
msgid "Previous page"
msgstr "Páxina anterior"
msgid "Next page"
msgstr "Páxina seguinte"
msgid "{{ user }} mentioned you."
msgstr "{{ user }} mencionouna."
msgid "{{ user }} commented your article."
msgstr "{{ user }} comentou o artigo."
msgid "{{ user }} is now following you."
msgstr "{{ user }} está a seguila."
msgid "{{ user }} liked your article."
msgstr "{{ user }} gustou do seu artigo."
msgid "{{ user }} reshared your article."
msgstr "{{ user }} comparteu o seu artigo."
msgid "Source code"
msgstr "Código fonte"
msgid "Matrix room"
msgstr "Sala Matrix"
msgid "Administration"
msgstr "Administración"
msgid "Instance settings"
msgstr "Axustes da instancia"
msgid "Allow anyone to register"
msgstr "Permitir o rexistro aberto"
msgid "Short description"
msgstr "Descrición curta"
msgid "Markdown is supported"
msgstr "Escritura Markdown activada"
msgid "Long description"
msgstr "Descrición longa"
msgid "Default license"
msgstr "Licenza por omisión"
msgid "Save settings"
msgstr "Gardar axustes"
msgid "No comments yet. Be the first to react!"
msgstr "Sin comentarios. Sexa a primeira e comentar!"
msgid "About this instance"
msgstr "Sobre esta instancia"
msgid "What is Plume?"
msgstr "Qué é Plume?"
msgid "Plume is a decentralized blogging engine."
msgstr "Plume é un motor de publicación descentralizada."
msgid "Authors can manage various blogs from an unique website."
msgstr "As autoras poden xestionar varios blogs desde un único sitio web."
msgid ""
"Articles are also visible on other Plume websites, and you can interact with "
"them directly from other platforms like Mastodon."
msgstr "Os artigos son visibles tamén en outros sitios Plume, e pode interactuar con"
"eles desde outras plataformas como Mastadon."
msgid "Create your account"
msgstr "Cree a súa conta"
msgid "About {{ instance_name }}"
msgstr "Acerca de {{ instance_name }}"
msgid "Home to"
msgstr "Fogar de"
msgid "people"
msgstr "persoas"
msgid "Who wrote"
msgstr "Que escribiron"
msgid "articles"
msgstr "artigos"
msgid "Read the detailed rules"
msgstr "Lea o detalle das normas"

437
po/nb.po Normal file
View File

@ -0,0 +1,437 @@
msgid ""
msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2018-06-15 16:33-0700\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: nb\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Latest articles"
msgstr "Siste artikler"
msgid "No posts to see here yet."
msgstr "Ingen innlegg å vise enda."
msgid "New article"
msgstr "Ny artikkel"
msgid "New blog"
msgstr "Ny blogg"
msgid "Create a blog"
msgstr "Lag en ny blogg"
msgid "Title"
msgstr "Tittel"
msgid "Create blog"
msgstr "Opprett blogg"
msgid "Comment \"{{ post }}\""
msgstr "Kommentér \"{{ post }}\""
msgid "Content"
msgstr "Innhold"
msgid "Submit comment"
msgstr "Send kommentar"
msgid "Something broke on our side."
msgstr "Noe gikk feil i vår ende."
msgid "Sorry about that. If you think this is a bug, please report it."
msgstr ""
"Beklager så mye. Dersom du tror dette er en bug, vær grei å rapportér det "
"til oss."
msgid "Configuration"
msgstr "Oppsett"
msgid "Configure your instance"
msgstr "Sett opp din instans"
msgid "Name"
msgstr "Navn"
msgid "Let&#x27;s go!"
msgstr "Kjør på!"
msgid "Welcome on {{ instance_name | escape }}"
msgstr "Velkommen til {{ instance_name | escape }}"
msgid "Notifications"
msgstr "Meldinger"
msgid ""
"Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}"
msgstr ""
"Skrevet av {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape}}{{ link_3 }}"
msgid "This article is under the {{ license }} license."
msgstr "Denne artikkelen er publisert med lisensen {{ license }}"
msgid "One like"
msgid_plural "{{ count }} likes"
msgstr[0] "Ett hjerte"
msgstr[1] "{{ count }} hjerter"
msgid "I don&#x27;t like this anymore"
msgstr "Jeg liker ikke dette lengre"
msgid "Add yours"
msgstr "Legg til din"
msgid "One reshare"
msgid_plural "{{ count }} reshares"
msgstr[0] "Én deling"
msgstr[1] "{{ count }} delinger"
msgid "I don&#x27;t want to reshare this anymore"
msgstr "Jeg ønsker ikke å dele dette lengre"
msgid "Reshare"
msgstr "Del"
msgid "Comments"
msgstr "Kommetarer"
msgid "Respond"
msgstr "Svar"
msgid "Comment"
msgstr "Kommentér"
msgid "New post"
msgstr "Nytt innlegg"
msgid "Create a post"
msgstr "Lag et nytt innlegg"
msgid "Publish"
msgstr "Publisér"
msgid "Login"
msgstr "Logg inn"
msgid "Username or email"
msgstr "Brukernavn eller epost"
msgid "Password"
msgstr "Passord"
msgid "Dashboard"
msgstr "Oversikt"
msgid "Your Dashboard"
msgstr "Din oversikt"
msgid "Your Blogs"
msgstr "Dine blogger"
msgid "You don&#x27;t have any blog yet. Create your own, or ask to join one."
msgstr ""
"Du har ingen blogger enda. Lag din egen, eller be om å få bli med på en "
"annen."
msgid "Start a new blog"
msgstr "Start en ny blogg"
msgid "Admin"
msgstr "Administrator"
msgid "It is you"
msgstr "Dette er deg"
msgid "Edit your profile"
msgstr "Rediger profilen din"
msgid "Open on {{ instance_url }}"
msgstr "Åpne hos {{ instance_url }}"
msgid "Follow"
msgstr "Følg"
msgid "Unfollow"
msgstr "Slutt å følge"
msgid "Recently reshared"
msgstr "Nylig delt"
msgid "One follower"
msgid_plural "{{ count }} followers"
msgstr[0] "Én følger"
msgstr[1] "{{ count }} følgere"
msgid "Edit your account"
msgstr "Rediger kontoen din"
msgid "Your Profile"
msgstr "Din profil"
msgid "Display Name"
msgstr "Visningsnavn"
msgid "Email"
msgstr "Epost"
msgid "Summary"
msgstr "Sammendrag"
msgid "Update account"
msgstr "Oppdater konto"
msgid "{{ name | escape }}'s followers"
msgstr "{{ name | escape}} sine følgere"
msgid "Followers"
msgstr "Følgere"
msgid "New Account"
msgstr "Ny konto"
msgid "Create an account"
msgstr "Lag en ny konto"
msgid "Username"
msgstr "Brukernavn"
msgid "Password confirmation"
msgstr "Passordbekreftelse"
msgid "Create account"
msgstr "Opprett konto"
msgid "Plume"
msgstr "Plume"
msgid "Menu"
msgstr "Meny"
msgid "My account"
msgstr "Min konto"
msgid "Log Out"
msgstr "Logg ut"
msgid "Log In"
msgstr "Logg inn"
msgid "Register"
msgstr "Registrér deg"
msgid "You need to be logged in order to create a new blog"
msgstr "Du må være logget inn for å lage en ny blogg"
msgid "You need to be logged in order to post a comment"
msgstr "Du må være logget inn for å legge inn en kommentar"
msgid "You need to be logged in order to like a post"
msgstr "Du må være logget inn for å like et innlegg"
msgid "You need to be logged in order to see your notifications"
msgstr "Du må være logget inn for å se meldingene dine"
msgid "You need to be logged in order to write a new post"
msgstr "Du må være logget inn for å skrive et nytt innlegg"
msgid "You need to be logged in order to reshare a post"
msgstr "Du må være logget inn for å dele et innlegg"
msgid "Invalid username or password"
msgstr "Ugyldig brukernavn eller passord"
msgid "You need to be logged in order to access your dashboard"
msgstr "Du må være logget inn for å få tilgang til oversikten din"
msgid "You need to be logged in order to follow someone"
msgstr "Du må være logget inn for å følge noen"
msgid "You need to be logged in order to edit your profile"
msgstr "Du må være logget inn for å redigere profilen din"
msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}"
msgstr "Av {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}"
msgid "{{ data }} reshared your article"
msgstr "{{ data }} delte din artikkel"
msgid "{{ data }} started following you"
msgstr "{{ data }} har begynt å følge deg"
msgid "{{ data }} liked your article"
msgstr "{{ data }} likte artikkelen din"
msgid "{{ data }} commented your article"
msgstr "{{ data }} la inn en kommentar til artikkelen din"
msgid "We couldn&#x27;t find this page."
msgstr "Den siden fant vi ikke."
msgid "The link that led you here may be broken."
msgstr "Kanhende lenken som førte deg hit er ødelagt."
msgid "You are not authorized."
msgstr "Det har du har ikke tilgang til."
msgid "You are not author in this blog."
msgstr "Du er ikke denne bloggens forfatter."
msgid "{{ data }} mentioned you."
msgstr "{{ data }} nevnte deg."
msgid "Your comment"
msgstr "Din kommentar"
msgid "Unknown error"
msgstr "Ukjent feil"
msgid "Invalid name"
msgstr "Ugyldig navn"
msgid "A blog with the same name already exists."
msgstr "En blogg med det navnet eksisterer allerede."
msgid "Your comment can't be empty"
msgstr "Kommentaren din kan ikke være tom"
msgid "A post with the same title already exists."
msgstr "Et innlegg med samme navn finnes allerede."
msgid "We need an email or a username to identify you"
msgstr "Vi trenger en epost eller et brukernavn for å identifisere deg"
#, fuzzy
msgid "Your password can't be empty"
msgstr "Kommentaren din kan ikke være tom"
msgid "Passwords are not matching"
msgstr "Passordene stemmer ikke overens"
msgid "Username can't be empty"
msgstr "Brukernavnet kan ikke være tomt"
msgid "Invalid email"
msgstr "Ugyldig epost"
msgid "Password should be at least 8 characters long"
msgstr "Passord må bestå av minst åtte tegn"
msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: "
msgstr[0] "Én forfatter av denne bloggen: "
msgstr[1] "{{ count }} forfattere av denne bloggen: "
msgid "Login or use your Fediverse account to interact with this article"
msgstr ""
"Logg inn eller bruk din Fediverse-konto for å gjøre noe med denne artikkelen"
msgid "Optional"
msgstr "Valgfritt"
msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog"
msgstr[0] "Én artikkel i denne bloggen"
msgstr[1] "{{ count }} artikler i denne bloggen"
msgid "Previous page"
msgstr "Forrige side"
msgid "Next page"
msgstr "Neste side"
msgid "{{ user }} mentioned you."
msgstr "{{ user }} nevnte deg."
msgid "{{ user }} commented your article."
msgstr "{{ user }} la igjen en kommentar til artikkelen din."
msgid "{{ user }} is now following you."
msgstr "{{ user }} har nå begynt å følge deg."
msgid "{{ user }} liked your article."
msgstr "{{ user }} likte artikkelen din."
msgid "{{ user }} reshared your article."
msgstr "{{ user }} delte artikkelen din med sine følgere."
msgid "Source code"
msgstr "Kildekode"
msgid "Matrix room"
msgstr "Snakkerom"
msgid "Administration"
msgstr "Administrasjon"
msgid "Instance settings"
msgstr "Instillinger for instansen"
msgid "Allow anyone to register"
msgstr "Tillat at hvem som helst registrerer seg"
msgid "Short description"
msgstr "Kort beskrivelse"
msgid "Markdown is supported"
msgstr "Du kan bruke markdown"
msgid "Long description"
msgstr "Lang beskrivelse"
msgid "Default license"
msgstr "Standardlisens"
msgid "Save settings"
msgstr "Lagre innstillingene"
msgid "No comments yet. Be the first to react!"
msgstr "Ingen kommentarer enda. Vær den første!"
msgid "About this instance"
msgstr "Om denne instansen"
msgid "What is Plume?"
msgstr "Hva er Plume?"
msgid "Plume is a decentralized blogging engine."
msgstr "Plume er et desentralisert bloggsystem."
msgid "Authors can manage various blogs from an unique website."
msgstr "Forfattere kan administrere forskjellige blogger fra en unik webside."
msgid ""
"Articles are also visible on other Plume websites, and you can interact with "
"them directly from other platforms like Mastodon."
msgstr ""
"Artiklene er også synlige på andre websider som kjører Plume, og du kan "
"interagere med dem direkte fra andre plattformer som f.eks. Mastodon."
msgid "Create your account"
msgstr "Opprett din konto"
msgid "About {{ instance_name }}"
msgstr "Om {{ instance_name }}"
msgid "Home to"
msgstr "Hjem for"
msgid "people"
msgstr "personer"
msgid "Who wrote"
msgstr "Som har skrevet"
msgid "articles"
msgstr "artikler"
msgid "Read the detailed rules"
msgstr "Les reglene"
#~ msgid "Your password should be at least 8 characters long"
#~ msgstr "Passordet ditt må bestå av minst åtte tegn"

156
po/pl.po
View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2018-06-29 23:02+0200\n"
"PO-Revision-Date: 2018-07-28 14:56+0200\n"
"Last-Translator: Marcin Mikołajczak <me@m4sk.in>\n"
"Language-Team: none\n"
"Language: pl\n"
@ -12,7 +12,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
"X-Generator: Poedit 2.0.8\n"
"X-Generator: Poedit 2.0.9\n"
msgid "Latest articles"
msgstr "Najnowsze artykuły"
@ -295,5 +295,157 @@ msgstr "{{ data }} wspomniał o Tobie."
msgid "Your comment"
msgstr "Twój komentarz"
msgid "Unknown error"
msgstr "Nieznany błąd"
msgid "Invalid name"
msgstr "Nieprawidłowa nazwa"
msgid "A blog with the same name already exists."
msgstr "Blog o tej nazwie już istnieje."
msgid "Your comment can't be empty"
msgstr "Twój komentarz nie może być pusty"
msgid "A post with the same title already exists."
msgstr "Wpis o tym tytule już istnieje."
msgid "We need an email or a username to identify you"
msgstr ""
"Potrzebujemy nazwy użytkownika lub adresu e-mail, aby Cię zidentyfikować"
#, fuzzy
msgid "Your password can't be empty"
msgstr "Twój komentarz nie może być pusty"
msgid "Passwords are not matching"
msgstr "Hasła nie pasują do siebie"
msgid "Username can't be empty"
msgstr "Nazwa użytkownika nie może być pusta"
msgid "Invalid email"
msgstr "Nieprawidłowy adres e-mail"
msgid "Password should be at least 8 characters long"
msgstr "Hasło musi składać się z przynajmniej 8 znaków"
msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: "
msgstr[0] "Ten blog ma jednego autora: "
msgstr[1] "Ten blog ma {{ count }} autorów: "
msgstr[2] "Ten blog ma {{ count }} autorów: "
msgid "Login or use your Fediverse account to interact with this article"
msgstr ""
"Zaloguj się lub użyj konta w Fediwersum, aby wejść w interakcje z tym "
"artykułem"
msgid "Optional"
msgstr "Nieobowiązkowe"
msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog"
msgstr[0] "Jeden artykuł na tym blogu"
msgstr[1] "{{ count }} artykuły na tym blogu"
msgstr[2] "{{ count }} artykułów na tym blogu"
msgid "Previous page"
msgstr "Poprzednia strona"
msgid "Next page"
msgstr "Następna strona"
msgid "{{ user }} mentioned you."
msgstr "{{ user }} wspomniał o Tobie."
msgid "{{ user }} commented your article."
msgstr "{{ user }} skomentował Twój artykuł."
msgid "{{ user }} is now following you."
msgstr "{{ user }} zaczął Cię obserwować."
msgid "{{ user }} liked your article."
msgstr "{{ user }} polubił Twój artykuł."
msgid "{{ user }} reshared your article."
msgstr "{{ user }} udostępnił Twój artykuł."
msgid "Source code"
msgstr "Kod źródłowy"
msgid "Matrix room"
msgstr "Pokój Matrix.org"
msgid "Administration"
msgstr "Administracja"
msgid "Instance settings"
msgstr "Ustawienia instancji"
msgid "Allow anyone to register"
msgstr "Pozwól każdemu na rejestrację"
msgid "Short description"
msgstr "Krótki opis"
msgid "Markdown is supported"
msgstr "Markdown jest obsługiwany"
msgid "Long description"
msgstr "Szczegółowy opis"
msgid "Default license"
msgstr "Domyślna licencja"
msgid "Save settings"
msgstr "Zapisz ustawienia"
msgid "No comments yet. Be the first to react!"
msgstr "Brak komentarzy. Bądź pierwszy!"
msgid "About this instance"
msgstr "O tej instancji"
msgid "What is Plume?"
msgstr "Czym jest Plume?"
msgid "Plume is a decentralized blogging engine."
msgstr "Plume jest zdecentralizowanym silnikiem blogowym."
msgid "Authors can manage various blogs from an unique website."
msgstr "Autorzy mogą zarządzać blogami ze specjalnej strony."
msgid ""
"Articles are also visible on other Plume websites, and you can interact with "
"them directly from other platforms like Mastodon."
msgstr ""
"Artykuły są widoczne na innych stronach Plume, możesz też wejść w interakcje "
"z nimi na platformach takich jak Mastodon."
msgid "Create your account"
msgstr "Utwórz konto"
msgid "About {{ instance_name }}"
msgstr "O {{ instance_name }}"
msgid "Home to"
msgstr "Dom dla"
msgid "people"
msgstr "osób"
msgid "Who wrote"
msgstr "Którzy napisali"
msgid "articles"
msgstr "artykuły"
msgid "Read the detailed rules"
msgstr "Przeczytaj szczegółowe zasady"
#~ msgid "Your password should be at least 8 characters long"
#~ msgstr "Twoje hasło musi składać się przynajmniej z 8 znaków"
#~ msgid "Logowanie"
#~ msgstr "Zaloguj się"

View File

@ -281,3 +281,142 @@ msgstr ""
msgid "Your comment"
msgstr ""
msgid "Unknown error"
msgstr ""
msgid "Invalid name"
msgstr ""
msgid "A blog with the same name already exists."
msgstr ""
msgid "Your comment can't be empty"
msgstr ""
msgid "A post with the same title already exists."
msgstr ""
msgid "We need an email or a username to identify you"
msgstr ""
msgid "Your password can't be empty"
msgstr ""
msgid "Passwords are not matching"
msgstr ""
msgid "Username can't be empty"
msgstr ""
msgid "Invalid email"
msgstr ""
msgid "Password should be at least 8 characters long"
msgstr ""
msgid "One author in this blog: "
msgid_plural "{{ count }} authors in this blog: "
msgstr[0] ""
msgstr[1] ""
msgid "Login or use your Fediverse account to interact with this article"
msgstr ""
msgid "Optional"
msgstr ""
msgid "One article in this blog"
msgid_plural "{{ count }} articles in this blog"
msgstr[0] ""
msgstr[1] ""
msgid "Previous page"
msgstr ""
msgid "Next page"
msgstr ""
msgid "{{ user }} mentioned you."
msgstr ""
msgid "{{ user }} commented your article."
msgstr ""
msgid "{{ user }} is now following you."
msgstr ""
msgid "{{ user }} liked your article."
msgstr ""
msgid "{{ user }} reshared your article."
msgstr ""
msgid "Source code"
msgstr ""
msgid "Matrix room"
msgstr ""
msgid "Administration"
msgstr ""
msgid "Instance settings"
msgstr ""
msgid "Allow anyone to register"
msgstr ""
msgid "Short description"
msgstr ""
msgid "Markdown is supported"
msgstr ""
msgid "Long description"
msgstr ""
msgid "Default license"
msgstr ""
msgid "Save settings"
msgstr ""
msgid "No comments yet. Be the first to react!"
msgstr ""
msgid "About this instance"
msgstr ""
msgid "What is Plume?"
msgstr ""
msgid "Plume is a decentralized blogging engine."
msgstr ""
msgid "Authors can manage various blogs from an unique website."
msgstr ""
msgid "Articles are also visible on other Plume websites, and you can interact with them directly from other platforms like Mastodon."
msgstr ""
msgid "Create your account"
msgstr ""
msgid "About {{ instance_name }}"
msgstr ""
msgid "Home to"
msgstr ""
msgid "people"
msgstr ""
msgid "Who wrote"
msgstr ""
msgid "articles"
msgstr ""
msgid "Read the detailed rules"
msgstr ""

View File

@ -16,7 +16,7 @@ use plume_models::{
pub trait Inbox {
fn received(&self, conn: &PgConnection, act: serde_json::Value) -> Result<(), Error> {
let actor_id = Id::new(act["actor"].as_str().unwrap());
let actor_id = Id::new(act["actor"].as_str().unwrap_or_else(|| act["actor"]["id"].as_str().expect("No actor ID for incoming activity")));
match act["type"].as_str() {
Some(t) => {
match t {

View File

@ -15,12 +15,20 @@ extern crate rocket_contrib;
extern crate rocket_csrf;
extern crate rocket_i18n;
extern crate rpassword;
extern crate serde;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;
extern crate validator;
#[macro_use]
extern crate validator_derive;
extern crate webfinger;
extern crate workerpool;
use rocket_contrib::Template;
use rocket_csrf::CsrfFairingBuilder;
use workerpool::{Pool, thunk::ThunkWorker};
mod inbox;
mod setup;
@ -30,6 +38,7 @@ fn main() {
let pool = setup::check();
rocket::ignite()
.mount("/", routes![
routes::blogs::paginated_details,
routes::blogs::details,
routes::blogs::activity_details,
routes::blogs::outbox,
@ -39,13 +48,17 @@ fn main() {
routes::comments::create,
routes::instance::paginated_index,
routes::instance::index,
routes::instance::admin,
routes::instance::update_settings,
routes::instance::shared_inbox,
routes::instance::nodeinfo,
routes::likes::create,
routes::likes::create_auth,
routes::notifications::paginated_notifications,
routes::notifications::notifications,
routes::notifications::notifications_auth,
@ -70,6 +83,7 @@ fn main() {
routes::user::details,
routes::user::dashboard,
routes::user::dashboard_auth,
routes::user::followers_paginated,
routes::user::followers,
routes::user::edit,
routes::user::edit_auth,
@ -94,6 +108,7 @@ fn main() {
routes::errors::server_error
])
.manage(pool)
.manage(Pool::<ThunkWorker<()>>::new(4))
.attach(Template::custom(|engines| {
rocket_i18n::tera(&mut engines.tera);
}))

View File

@ -5,8 +5,10 @@ use rocket::{
};
use rocket_contrib::Template;
use serde_json;
use std::{collections::HashMap, borrow::Cow};
use validator::{Validate, ValidationError, ValidationErrors};
use plume_common::activity_pub::ActivityStream;
use plume_common::activity_pub::{ActivityStream, ApRequest};
use plume_common::utils;
use plume_models::{
blog_authors::*,
@ -16,23 +18,36 @@ use plume_models::{
posts::Post,
users::User
};
use routes::Page;
#[get("/~/<name>", rank = 2)]
fn details(name: String, conn: DbConn, user: Option<User>) -> Template {
#[get("/~/<name>?<page>", rank = 2)]
fn paginated_details(name: String, conn: DbConn, user: Option<User>, page: Page) -> Template {
may_fail!(user, Blog::find_by_fqn(&*conn, name), "Requested blog couldn't be found", |blog| {
let recents = Post::get_recents_for_blog(&*conn, &blog, 5);
let posts = Post::blog_page(&*conn, &blog, page.limits());
let articles = Post::get_for_blog(&*conn, &blog);
let authors = &blog.list_authors(&*conn);
Template::render("blogs/details", json!({
"blog": blog,
"blog": &blog.to_json(&*conn),
"account": user,
"is_author": user.map(|x| x.is_author_in(&*conn, blog)),
"recents": recents.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>()
"is_author": user.map(|x| x.is_author_in(&*conn, blog.clone())),
"posts": posts.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
"authors": authors.into_iter().map(|u| u.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
"n_authors": authors.len(),
"n_articles": articles.len(),
"page": page.page,
"n_pages": Page::total(articles.len() as i32)
}))
})
}
#[get("/~/<name>", format = "application/activity+json", rank = 1)]
fn activity_details(name: String, conn: DbConn) -> ActivityStream<CustomGroup> {
#[get("/~/<name>", rank = 3)]
fn details(name: String, conn: DbConn, user: Option<User>) -> Template {
paginated_details(name, conn, user, Page::first())
}
#[get("/~/<name>", rank = 1)]
fn activity_details(name: String, conn: DbConn, _ap: ApRequest) -> ActivityStream<CustomGroup> {
let blog = Blog::find_local(&*conn, name).unwrap();
ActivityStream::new(blog.into_activity(&*conn))
}
@ -40,7 +55,9 @@ fn activity_details(name: String, conn: DbConn) -> ActivityStream<CustomGroup> {
#[get("/blogs/new")]
fn new(user: User) -> Template {
Template::render("blogs/new", json!({
"account": user
"account": user,
"errors": null,
"form": null
}))
}
@ -49,21 +66,41 @@ fn new_auth() -> Flash<Redirect>{
utils::requires_login("You need to be logged in order to create a new blog", uri!(new))
}
#[derive(FromForm)]
#[derive(FromForm, Validate, Serialize)]
struct NewBlogForm {
#[validate(custom(function = "valid_slug", message = "Invalid name"))]
pub title: String
}
fn valid_slug(title: &str) -> Result<(), ValidationError> {
let slug = utils::make_actor_id(title.to_string());
if slug.len() == 0 {
Err(ValidationError::new("empty_slug"))
} else {
Ok(())
}
}
#[post("/blogs/new", data = "<data>")]
fn create(conn: DbConn, data: LenientForm<NewBlogForm>, user: User) -> Redirect {
fn create(conn: DbConn, data: LenientForm<NewBlogForm>, user: User) -> Result<Redirect, Template> {
let form = data.get();
let slug = utils::make_actor_id(form.title.to_string());
if Blog::find_local(&*conn, slug.clone()).is_some() || slug.len() == 0 {
Redirect::to(uri!(new))
} else {
let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(),
Err(e) => e
};
if let Some(_) = Blog::find_local(&*conn, slug.clone()) {
errors.add("title", ValidationError {
code: Cow::from("existing_slug"),
message: Some(Cow::from("A blog with the same name already exists.")),
params: HashMap::new()
});
}
if errors.is_empty() {
let blog = Blog::insert(&*conn, NewBlog::new_local(
slug.to_string(),
slug.clone(),
form.title.to_string(),
String::from(""),
Instance::local_id(&*conn)
@ -76,7 +113,14 @@ fn create(conn: DbConn, data: LenientForm<NewBlogForm>, user: User) -> Redirect
is_owner: true
});
Redirect::to(uri!(details: name = slug))
Ok(Redirect::to(uri!(details: name = slug.clone())))
} else {
println!("{:?}", errors);
Err(Template::render("blogs/new", json!({
"account": user,
"errors": errors.inner(),
"form": form
})))
}
}

View File

@ -1,8 +1,12 @@
use rocket::{
State,
request::LenientForm,
response::Redirect
};
use rocket_contrib::Template;
use serde_json;
use validator::Validate;
use workerpool::{Pool, thunk::*};
use plume_common::activity_pub::broadcast;
use plume_models::{
@ -15,30 +19,55 @@ use plume_models::{
};
use inbox::Inbox;
#[derive(FromForm, Debug)]
#[derive(FromForm, Debug, Validate)]
struct NewCommentForm {
pub responding_to: Option<i32>,
#[validate(length(min = "1", message = "Your comment can't be empty"))]
pub content: String
}
#[post("/~/<blog_name>/<slug>/comment", data = "<data>")]
fn create(blog_name: String, slug: String, data: LenientForm<NewCommentForm>, user: User, conn: DbConn) -> Redirect {
fn create(blog_name: String, slug: String, data: LenientForm<NewCommentForm>, user: User, conn: DbConn, worker: State<Pool<ThunkWorker<()>>>) -> Result<Redirect, Template> {
let blog = Blog::find_by_fqn(&*conn, blog_name.clone()).unwrap();
let post = Post::find_by_slug(&*conn, slug.clone(), blog.id).unwrap();
let form = data.get();
println!("form: {:?}", form);
form.validate()
.map(|_| {
let (new_comment, id) = NewComment::build()
.content(form.content.clone())
.in_response_to_id(form.responding_to.clone())
.post(post.clone())
.author(user.clone())
.create(&*conn);
let (new_comment, id) = NewComment::build()
.content(form.content.clone())
.in_response_to_id(form.responding_to.clone())
.post(post)
.author(user.clone())
.create(&*conn);
let instance = Instance::get_local(&*conn).unwrap();
instance.received(&*conn, serde_json::to_value(new_comment.clone()).expect("JSON serialization error"))
.expect("We are not compatible with ourselve: local broadcast failed (new comment)");
let followers = user.get_followers(&*conn);
let user_clone = user.clone();
worker.execute(Thunk::of(move || broadcast(&user_clone, new_comment, followers)));
let instance = Instance::get_local(&*conn).unwrap();
instance.received(&*conn, serde_json::to_value(new_comment.clone()).expect("JSON serialization error"))
.expect("We are not compatible with ourselve: local broadcast failed (new comment)");
broadcast(&user, new_comment, user.get_followers(&*conn));
Redirect::to(format!("/~/{}/{}/#comment-{}", blog_name, slug, id))
})
.map_err(|errors| {
// TODO: de-duplicate this code
let comments = Comment::list_by_post(&*conn, post.id);
let comms = comments.clone();
Redirect::to(format!("/~/{}/{}/#comment-{}", blog_name, slug, id))
Template::render("posts/details", json!({
"author": post.get_authors(&*conn)[0].to_json(&*conn),
"post": post,
"blog": blog,
"comments": &comments.into_iter().map(|c| c.to_json(&*conn, &comms)).collect::<Vec<serde_json::Value>>(),
"n_likes": post.get_likes(&*conn).len(),
"has_liked": user.has_liked(&*conn, &post),
"n_reshares": post.get_reshares(&*conn).len(),
"has_reshared": user.has_reshared(&*conn, &post),
"account": user,
"date": &post.creation_date.timestamp(),
"previous": form.responding_to.map(|r| Comment::get(&*conn, r).expect("Error retrieving previous comment").to_json(&*conn, &vec![])),
"user_fqn": user.get_fqn(&*conn),
"errors": errors
}))
})
}

View File

@ -1,8 +1,11 @@
use gettextrs::gettext;
use rocket::{request::LenientForm, response::Redirect};
use rocket_contrib::{Json, Template};
use serde_json;
use validator::{Validate};
use plume_models::{
admin::Admin,
comments::Comment,
db_conn::DbConn,
posts::Post,
@ -10,17 +13,22 @@ use plume_models::{
instance::*
};
use inbox::Inbox;
use routes::Page;
#[get("/")]
fn index(conn: DbConn, user: Option<User>) -> Template {
#[get("/?<page>")]
fn paginated_index(conn: DbConn, user: Option<User>, page: Page) -> Template {
match Instance::get_local(&*conn) {
Some(inst) => {
let recents = Post::get_recents(&*conn, 6);
let recents = Post::get_recents_page(&*conn, page.limits());
Template::render("instance/index", json!({
"instance": inst,
"account": user,
"recents": recents.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>()
"recents": recents.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
"page": page.page,
"n_pages": Page::total(Post::count(&*conn) as i32),
"n_users": User::count_local(&*conn),
"n_articles": Post::count_local(&*conn)
}))
}
None => {
@ -31,6 +39,53 @@ fn index(conn: DbConn, user: Option<User>) -> Template {
}
}
#[get("/")]
fn index(conn: DbConn, user: Option<User>) -> Template {
paginated_index(conn, user, Page::first())
}
#[get("/admin")]
fn admin(conn: DbConn, admin: Admin) -> Template {
Template::render("instance/admin", json!({
"account": admin.0,
"instance": Instance::get_local(&*conn),
"errors": null,
"form": null
}))
}
#[derive(FromForm, Validate, Serialize)]
struct InstanceSettingsForm {
#[validate(length(min = "1"))]
name: String,
open_registrations: bool,
short_description: String,
long_description: String,
#[validate(length(min = "1"))]
default_license: String
}
#[post("/admin", data = "<form>")]
fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSettingsForm>) -> Result<Redirect, Template> {
let form = form.get();
form.validate()
.map(|_| {
let instance = Instance::get_local(&*conn).unwrap();
instance.update(&*conn,
form.name.clone(),
form.open_registrations,
form.short_description.clone(),
form.long_description.clone());
Redirect::to(uri!(admin))
})
.map_err(|e| Template::render("instance/admin", json!({
"account": admin.0,
"instance": Instance::get_local(&*conn),
"errors": e.inner(),
"form": form
})))
}
#[post("/inbox", data = "<data>")]
fn shared_inbox(conn: DbConn, data: String) -> String {
let act: serde_json::Value = serde_json::from_str(&data[..]).unwrap();

View File

@ -1,4 +1,5 @@
use rocket::response::{Redirect, Flash};
use rocket::{State, response::{Redirect, Flash}};
use workerpool::{Pool, thunk::*};
use plume_common::activity_pub::{broadcast, inbox::Notify};
use plume_common::utils;
@ -10,8 +11,8 @@ use plume_models::{
users::User
};
#[get("/~/<blog>/<slug>/like")]
fn create(blog: String, slug: String, user: User, conn: DbConn) -> Redirect {
#[post("/~/<blog>/<slug>/like")]
fn create(blog: String, slug: String, user: User, conn: DbConn, worker: State<Pool<ThunkWorker<()>>>) -> Redirect {
let b = Blog::find_by_fqn(&*conn, blog.clone()).unwrap();
let post = Post::find_by_slug(&*conn, slug.clone(), b.id).unwrap();
@ -24,17 +25,20 @@ fn create(blog: String, slug: String, user: User, conn: DbConn) -> Redirect {
like.update_ap_url(&*conn);
like.notify(&*conn);
broadcast(&user, like.into_activity(&*conn), user.get_followers(&*conn));
let followers = user.get_followers(&*conn);
let act = like.into_activity(&*conn);
worker.execute(Thunk::of(move || broadcast(&user, act, followers)));
} else {
let like = likes::Like::find_by_user_on_post(&*conn, user.id, post.id).unwrap();
let delete_act = like.delete(&*conn);
broadcast(&user, delete_act, user.get_followers(&*conn));
let followers = user.get_followers(&*conn);
worker.execute(Thunk::of(move || broadcast(&user, delete_act, followers)));
}
Redirect::to(uri!(super::posts::details: blog = blog, slug = slug))
}
#[get("/~/<blog>/<slug>/like", rank = 2)]
#[post("/~/<blog>/<slug>/like", rank = 2)]
fn create_auth(blog: String, slug: String) -> Flash<Redirect>{
utils::requires_login("You need to be logged in order to like a post", uri!(create: blog = blog, slug = slug))
}

View File

@ -1,5 +1,11 @@
use rocket::response::NamedFile;
use std::path::{Path, PathBuf};
use rocket::{
http::uri::{FromUriParam, UriDisplay},
response::NamedFile
};
use std::{
fmt,
path::{Path, PathBuf}
};
macro_rules! may_fail {
($account:expr, $expr:expr, $template:expr, $msg:expr, | $res:ident | $block:block) => {
@ -28,6 +34,47 @@ macro_rules! may_fail {
};
}
const ITEMS_PER_PAGE: i32 = 10;
#[derive(FromForm)]
pub struct Page {
page: i32
}
impl UriDisplay for Page {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "page={}", &self.page as &UriDisplay)
}
}
impl FromUriParam<i32> for Page {
type Target = Page;
fn from_uri_param(num: i32) -> Page {
Page { page: num }
}
}
impl Page {
pub fn first() -> Page {
Page {
page: 1
}
}
/// Computes the total number of pages needed to display n_items
pub fn total(n_items: i32) -> i32 {
if n_items % ITEMS_PER_PAGE == 0 {
n_items / ITEMS_PER_PAGE
} else {
(n_items / ITEMS_PER_PAGE) + 1
}
}
pub fn limits(&self) -> (i32, i32) {
((self.page - 1) * ITEMS_PER_PAGE, self.page * ITEMS_PER_PAGE)
}
}
pub mod blogs;
pub mod comments;
pub mod errors;

View File

@ -3,13 +3,21 @@ use rocket_contrib::Template;
use plume_common::utils;
use plume_models::{db_conn::DbConn, notifications::Notification, users::User};
use routes::Page;
#[get("/notifications?<page>")]
fn paginated_notifications(conn: DbConn, user: User, page: Page) -> Template {
Template::render("notifications/index", json!({
"account": user,
"notifications": Notification::page_for_user(&*conn, &user, page.limits()).into_iter().map(|n| n.to_json(&*conn)).collect::<Vec<_>>(),
"page": page.page,
"n_pages": Page::total(Notification::find_for_user(&*conn, &user).len() as i32)
}))
}
#[get("/notifications")]
fn notifications(conn: DbConn, user: User) -> Template {
Template::render("notifications/index", json!({
"account": user,
"notifications": Notification::find_for_user(&*conn, &user)
}))
paginated_notifications(conn, user, Page::first())
}
#[get("/notifications", rank = 2)]

View File

@ -1,16 +1,20 @@
use activitypub::object::Article;
use heck::KebabCase;
use rocket::request::LenientForm;
use rocket::{State, request::LenientForm};
use rocket::response::{Redirect, Flash};
use rocket_contrib::Template;
use serde_json;
use std::{collections::HashMap, borrow::Cow};
use validator::{Validate, ValidationError, ValidationErrors};
use workerpool::{Pool, thunk::*};
use plume_common::activity_pub::{broadcast, ActivityStream};
use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest};
use plume_common::utils;
use plume_models::{
blogs::*,
db_conn::DbConn,
comments::Comment,
instance::Instance,
mentions::Mention,
post_authors::*,
posts::*,
@ -34,27 +38,32 @@ fn details_response(blog: String, slug: String, conn: DbConn, user: Option<User>
may_fail!(user, Blog::find_by_fqn(&*conn, blog), "Couldn't find this blog", |blog| {
may_fail!(user, Post::find_by_slug(&*conn, slug, blog.id), "Couldn't find this post", |post| {
let comments = Comment::list_by_post(&*conn, post.id);
let comms = comments.clone();
Template::render("posts/details", json!({
"author": post.get_authors(&*conn)[0].to_json(&*conn),
"post": post,
"blog": blog,
"comments": comments.into_iter().map(|c| c.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
"article": post.to_json(&*conn),
"blog": blog.to_json(&*conn),
"comments": &comments.into_iter().filter_map(|c| if c.in_response_to_id.is_none() {
Some(c.to_json(&*conn, &comms))
} else {
None
}).collect::<Vec<serde_json::Value>>(),
"n_likes": post.get_likes(&*conn).len(),
"has_liked": user.clone().map(|u| u.has_liked(&*conn, &post)).unwrap_or(false),
"n_reshares": post.get_reshares(&*conn).len(),
"has_reshared": user.clone().map(|u| u.has_reshared(&*conn, &post)).unwrap_or(false),
"account": user,
"date": &post.creation_date.timestamp(),
"previous": query.and_then(|q| q.responding_to.map(|r| Comment::get(&*conn, r).expect("Error retrieving previous comment").to_json(&*conn))),
"previous": query.and_then(|q| q.responding_to.map(|r| Comment::get(&*conn, r).expect("Error retrieving previous comment").to_json(&*conn, &vec![]))),
"user_fqn": user.map(|u| u.get_fqn(&*conn)).unwrap_or(String::new())
}))
})
})
}
#[get("/~/<blog>/<slug>", rank = 3, format = "application/activity+json")]
fn activity_details(blog: String, slug: String, conn: DbConn) -> ActivityStream<Article> {
#[get("/~/<blog>/<slug>", rank = 3)]
fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest) -> ActivityStream<Article> {
let blog = Blog::find_by_fqn(&*conn, blog).unwrap();
let post = Post::find_by_slug(&*conn, slug, blog.id).unwrap();
@ -76,29 +85,55 @@ fn new(blog: String, user: User, conn: DbConn) -> Template {
}))
} else {
Template::render("posts/new", json!({
"account": user
"account": user,
"instance": Instance::get_local(&*conn),
"errors": null,
"form": null
}))
}
}
#[derive(FromForm)]
#[derive(FromForm, Validate, Serialize)]
struct NewPostForm {
#[validate(custom(function = "valid_slug", message = "Invalid title"))]
pub title: String,
pub content: String,
pub license: String
}
fn valid_slug(title: &str) -> Result<(), ValidationError> {
let slug = title.to_string().to_kebab_case();
if slug.len() == 0 {
Err(ValidationError::new("empty_slug"))
} else if slug == "new" {
Err(ValidationError::new("invalid_slug"))
} else {
Ok(())
}
}
#[post("/~/<blog_name>/new", data = "<data>")]
fn create(blog_name: String, data: LenientForm<NewPostForm>, user: User, conn: DbConn) -> Redirect {
fn create(blog_name: String, data: LenientForm<NewPostForm>, user: User, conn: DbConn, worker: State<Pool<ThunkWorker<()>>>) -> Result<Redirect, Template> {
let blog = Blog::find_by_fqn(&*conn, blog_name.to_string()).unwrap();
let form = data.get();
let slug = form.title.to_string().to_kebab_case();
if !user.is_author_in(&*conn, blog.clone()) {
Redirect::to(uri!(super::blogs::details: name = blog_name))
} else {
if slug == "new" || Post::find_by_slug(&*conn, slug.clone(), blog.id).is_some() {
Redirect::to(uri!(new: blog = blog_name))
let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(),
Err(e) => e
};
if let Some(_) = Post::find_by_slug(&*conn, slug.clone(), blog.id) {
errors.add("title", ValidationError {
code: Cow::from("existing_slug"),
message: Some(Cow::from("A post with the same title already exists.")),
params: HashMap::new()
});
}
if errors.is_empty() {
if !user.is_author_in(&*conn, blog.clone()) {
// actually it's not "Ok"…
Ok(Redirect::to(uri!(super::blogs::details: name = blog_name)))
} else {
let (content, mentions) = utils::md_to_html(form.content.to_string().as_ref());
@ -108,8 +143,13 @@ fn create(blog_name: String, data: LenientForm<NewPostForm>, user: User, conn: D
title: form.title.to_string(),
content: SafeString::new(&content),
published: true,
license: form.license.to_string(),
ap_url: "".to_string()
license: if form.license.len() > 0 {
form.license.to_string()
} else {
Instance::get_local(&*conn).map(|i| i.default_license).unwrap_or(String::from("CC-0"))
},
ap_url: "".to_string(),
creation_date: None
});
let post = post.update_ap_url(&*conn);
PostAuthor::insert(&*conn, NewPostAuthor {
@ -122,9 +162,17 @@ fn create(blog_name: String, data: LenientForm<NewPostForm>, user: User, conn: D
}
let act = post.create_activity(&*conn);
broadcast(&user, act, user.get_followers(&*conn));
let followers = user.get_followers(&*conn);
worker.execute(Thunk::of(move || broadcast(&user, act, followers)));
Redirect::to(uri!(details: blog = blog_name, slug = slug))
Ok(Redirect::to(uri!(details: blog = blog_name, slug = slug)))
}
} else {
Err(Template::render("posts/new", json!({
"account": user,
"instance": Instance::get_local(&*conn),
"errors": errors.inner(),
"form": form
})))
}
}

View File

@ -1,4 +1,5 @@
use rocket::response::{Redirect, Flash};
use rocket::{State, response::{Redirect, Flash}};
use workerpool::{Pool, thunk::*};
use plume_common::activity_pub::{broadcast, inbox::Notify};
use plume_common::utils;
@ -10,8 +11,8 @@ use plume_models::{
users::User
};
#[get("/~/<blog>/<slug>/reshare")]
fn create(blog: String, slug: String, user: User, conn: DbConn) -> Redirect {
#[post("/~/<blog>/<slug>/reshare")]
fn create(blog: String, slug: String, user: User, conn: DbConn, worker: State<Pool<ThunkWorker<()>>>) -> Redirect {
let b = Blog::find_by_fqn(&*conn, blog.clone()).unwrap();
let post = Post::find_by_slug(&*conn, slug.clone(), b.id).unwrap();
@ -24,17 +25,20 @@ fn create(blog: String, slug: String, user: User, conn: DbConn) -> Redirect {
reshare.update_ap_url(&*conn);
reshare.notify(&*conn);
broadcast(&user, reshare.into_activity(&*conn), user.get_followers(&*conn));
let followers = user.get_followers(&*conn);
let act = reshare.into_activity(&*conn);
worker.execute(Thunk::of(move || broadcast(&user, act, followers)));
} else {
let reshare = Reshare::find_by_user_on_post(&*conn, user.id, post.id).unwrap();
let delete_act = reshare.delete(&*conn);
broadcast(&user, delete_act, user.get_followers(&*conn));
let followers = user.get_followers(&*conn);
worker.execute(Thunk::of(move || broadcast(&user, delete_act, followers)));
}
Redirect::to(uri!(super::posts::details: blog = blog, slug = slug))
}
#[get("/~/<blog>/<slug>/reshare", rank=1)]
#[post("/~/<blog>/<slug>/reshare", rank=1)]
fn create_auth(blog: String, slug: String) -> Flash<Redirect> {
utils::requires_login("You need to be logged in order to reshare a post", uri!(create: blog = blog, slug = slug))
}

View File

@ -1,10 +1,11 @@
use gettextrs::gettext;
use rocket::{
http::{Cookie, Cookies, uri::Uri},
response::{Redirect, status::NotFound},
response::Redirect,
request::{LenientForm,FlashMessage}
};
use rocket_contrib::Template;
use std::borrow::Cow;
use validator::{Validate, ValidationError, ValidationErrors};
use plume_models::{
db_conn::DbConn,
@ -14,7 +15,9 @@ use plume_models::{
#[get("/login")]
fn new(user: Option<User>) -> Template {
Template::render("session/login", json!({
"account": user
"account": user,
"errors": null,
"form": null
}))
}
@ -27,40 +30,55 @@ struct Message {
fn new_message(user: Option<User>, message: Message) -> Template {
Template::render("session/login", json!({
"account": user,
"message": message.m
"message": message.m,
"errors": null,
"form": null
}))
}
#[derive(FromForm)]
#[derive(FromForm, Validate, Serialize)]
struct LoginForm {
#[validate(length(min = "1", message = "We need an email or a username to identify you"))]
email_or_name: String,
#[validate(length(min = "1", message = "Your password can't be empty"))]
password: String
}
#[post("/login", data = "<data>")]
fn create(conn: DbConn, data: LenientForm<LoginForm>, flash: Option<FlashMessage>, mut cookies: Cookies) -> Result<Redirect, NotFound<String>> {
fn create(conn: DbConn, data: LenientForm<LoginForm>, flash: Option<FlashMessage>, mut cookies: Cookies) -> Result<Redirect, Template> {
let form = data.get();
let user = match User::find_by_email(&*conn, form.email_or_name.to_string()) {
Some(usr) => Ok(usr),
None => match User::find_local(&*conn, form.email_or_name.to_string()) {
Some(usr) => Ok(usr),
None => Err(gettext("Invalid username or password"))
}
let user = User::find_by_email(&*conn, form.email_or_name.to_string())
.map(|u| Ok(u))
.unwrap_or_else(|| User::find_local(&*conn, form.email_or_name.to_string()).map(|u| Ok(u)).unwrap_or(Err(())));
let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(),
Err(e) => e
};
match user {
Ok(usr) => {
if usr.auth(form.password.to_string()) {
cookies.add_private(Cookie::new(AUTH_COOKIE, usr.id.to_string()));
Ok(Redirect::to(Uri::new(flash
.and_then(|f| if f.name() == "callback" { Some(f.msg().to_owned()) } else { None })
.unwrap_or("/".to_owned()))
))
} else {
Err(NotFound(gettext("Invalid username or password")))
}
},
Err(e) => Err(NotFound(String::from(e)))
if let Err(_) = user.clone() {
let mut err = ValidationError::new("invalid_login");
err.message = Some(Cow::from("Invalid username or password"));
errors.add("email_or_name", err)
} else if !user.clone().expect("User not found").auth(form.password.clone()) {
let mut err = ValidationError::new("invalid_login");
err.message = Some(Cow::from("Invalid username or password"));
errors.add("email_or_name", err)
}
if errors.is_empty() {
cookies.add_private(Cookie::new(AUTH_COOKIE, user.unwrap().id.to_string()));
Ok(Redirect::to(Uri::new(flash
.and_then(|f| if f.name() == "callback" { Some(f.msg().to_owned()) } else { None })
.unwrap_or("/".to_owned()))
))
} else {
println!("{:?}", errors);
Err(Template::render("session/login", json!({
"account": null,
"errors": errors.inner(),
"form": form
})))
}
}

View File

@ -1,16 +1,21 @@
use activitypub::{
activity::Follow,
collection::OrderedCollection
activity::{Create, Follow},
collection::OrderedCollection,
object::Article
};
use rocket::{request::LenientForm,
use rocket::{
State,
request::LenientForm,
response::{Redirect, Flash}
};
use rocket_contrib::Template;
use serde_json;
use validator::{Validate, ValidationError};
use workerpool::{Pool, thunk::*};
use plume_common::activity_pub::{
ActivityStream, broadcast, Id, IntoId,
inbox::{Notify}
ActivityStream, broadcast, Id, IntoId, ApRequest,
inbox::{FromActivity, Notify}
};
use plume_common::utils;
use plume_models::{
@ -23,6 +28,7 @@ use plume_models::{
users::*
};
use inbox::Inbox;
use routes::Page;
#[get("/me")]
fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
@ -33,15 +39,44 @@ fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
}
#[get("/@/<name>", rank = 2)]
fn details(name: String, conn: DbConn, account: Option<User>) -> Template {
fn details<'r>(name: String, conn: DbConn, account: Option<User>, worker: State<Pool<ThunkWorker<()>>>, fecth_articles_conn: DbConn, fecth_followers_conn: DbConn) -> Template {
may_fail!(account, User::find_by_fqn(&*conn, name), "Couldn't find requested user", |user| {
let recents = Post::get_recents_for_author(&*conn, &user, 6);
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6);
let user_id = user.id.clone();
let n_followers = user.get_followers(&*conn).len();
if !user.get_instance(&*conn).local {
// Fetch new articles
let user_clone = user.clone();
worker.execute(Thunk::of(move || {
for create_act in user_clone.fetch_outbox::<Create>() {
match create_act.create_props.object_object::<Article>() {
Ok(article) => {
Post::from_activity(&*fecth_articles_conn, article, user_clone.clone().into_id());
println!("Fetched article from remote user");
}
Err(e) => println!("Error while fetching articles in background: {:?}", e)
}
}
}));
// Fetch followers
let user_clone = user.clone();
worker.execute(Thunk::of(move || {
for user_id in user_clone.fetch_followers_ids() {
let follower = User::find_by_ap_url(&*fecth_followers_conn, user_id.clone())
.unwrap_or_else(|| User::fetch_from_url(&*fecth_followers_conn, user_id).expect("Couldn't fetch follower"));
follows::Follow::insert(&*fecth_followers_conn, follows::NewFollow {
follower_id: follower.id,
following_id: user_clone.id
});
}
}));
}
Template::render("users/details", json!({
"user": serde_json::to_value(user.clone()).unwrap(),
"user": user.to_json(&*conn),
"instance_url": user.get_instance(&*conn).public_domain,
"is_remote": user.instance_id != Instance::local_id(&*conn),
"follows": account.clone().map(|x| x.is_following(&*conn, user.id)).unwrap_or(false),
@ -69,7 +104,7 @@ fn dashboard_auth() -> Flash<Redirect> {
}
#[get("/@/<name>/follow")]
fn follow(name: String, conn: DbConn, user: User) -> Redirect {
fn follow(name: String, conn: DbConn, user: User, worker: State<Pool<ThunkWorker<()>>>) -> Redirect {
let target = User::find_by_fqn(&*conn, name.clone()).unwrap();
let f = follows::Follow::insert(&*conn, follows::NewFollow {
follower_id: user.id,
@ -84,7 +119,7 @@ fn follow(name: String, conn: DbConn, user: User) -> Redirect {
act.object_props.set_to_link(target.clone().into_id()).expect("New Follow error while setting 'to'");
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("New Follow error while setting 'cc'");
broadcast(&user, act, vec![target]);
worker.execute(Thunk::of(move || broadcast(&user, act, vec![target])));
Redirect::to(uri!(details: name = name))
}
@ -93,26 +128,35 @@ fn follow_auth(name: String) -> Flash<Redirect> {
utils::requires_login("You need to be logged in order to follow someone", uri!(follow: name = name))
}
#[get("/@/<name>/followers", rank = 2)]
fn followers(name: String, conn: DbConn, account: Option<User>) -> Template {
#[get("/@/<name>/followers?<page>")]
fn followers_paginated(name: String, conn: DbConn, account: Option<User>, page: Page) -> Template {
may_fail!(account, User::find_by_fqn(&*conn, name.clone()), "Couldn't find requested user", |user| {
let user_id = user.id.clone();
let followers_count = user.get_followers(&*conn).len();
Template::render("users/followers", json!({
"user": serde_json::to_value(user.clone()).unwrap(),
"user": user.to_json(&*conn),
"instance_url": user.get_instance(&*conn).public_domain,
"is_remote": user.instance_id != Instance::local_id(&*conn),
"follows": account.clone().map(|x| x.is_following(&*conn, user.id)).unwrap_or(false),
"followers": user.get_followers(&*conn).into_iter().map(|f| f.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
"followers": user.get_followers_page(&*conn, page.limits()).into_iter().map(|f| f.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
"account": account,
"is_self": account.map(|a| a.id == user_id).unwrap_or(false),
"n_followers": user.get_followers(&*conn).len()
"n_followers": followers_count,
"page": page.page,
"n_pages": Page::total(followers_count as i32)
}))
})
}
#[get("/@/<name>", format = "application/activity+json", rank = 1)]
fn activity_details(name: String, conn: DbConn) -> ActivityStream<CustomPerson> {
#[get("/@/<name>/followers", rank = 2)]
fn followers(name: String, conn: DbConn, account: Option<User>) -> Template {
followers_paginated(name, conn, account, Page::first())
}
#[get("/@/<name>", rank = 1)]
fn activity_details(name: String, conn: DbConn, _ap: ApRequest) -> ActivityStream<CustomPerson> {
let user = User::find_local(&*conn, name).unwrap();
ActivityStream::new(user.into_activity(&*conn))
}
@ -120,7 +164,9 @@ fn activity_details(name: String, conn: DbConn) -> ActivityStream<CustomPerson>
#[get("/users/new")]
fn new(user: Option<User>) -> Template {
Template::render("users/new", json!({
"account": user
"account": user,
"errors": null,
"form": null
}))
}
@ -157,40 +203,49 @@ fn update(_name: String, conn: DbConn, user: User, data: LenientForm<UpdateUserF
Redirect::to(uri!(me))
}
#[derive(FromForm)]
#[derive(FromForm, Serialize, Validate)]
#[validate(schema(function = "passwords_match", skip_on_field_errors = "false", message = "Passwords are not matching"))]
struct NewUserForm {
#[validate(length(min = "1", message = "Username can't be empty"))]
username: String,
#[validate(email(message = "Invalid email"))]
email: String,
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
password: String,
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
password_confirmation: String
}
#[post("/users/new", data = "<data>")]
fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, String> {
let form = data.get();
if form.username.clone().len() < 1 {
Err(String::from("Username is required"))
} else if form.email.clone().len() < 1 {
Err(String::from("Email is required"))
} else if form.password.clone().len() < 8 {
Err(String::from("Password should be at least 8 characters long"))
} else if form.password == form.password_confirmation {
NewUser::new_local(
&*conn,
form.username.to_string(),
form.username.to_string(),
false,
String::from(""),
form.email.to_string(),
User::hash_pass(form.password.to_string())
).update_boxes(&*conn);
Ok(Redirect::to(uri!(super::session::new)))
fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
if form.password != form.password_confirmation {
Err(ValidationError::new("password_match"))
} else {
Err(String::from("Passwords don't match"))
Ok(())
}
}
#[post("/users/new", data = "<data>")]
fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, Template> {
let form = data.get();
form.validate()
.map(|_| {
NewUser::new_local(
&*conn,
form.username.to_string(),
form.username.to_string(),
false,
String::from(""),
form.email.to_string(),
User::hash_pass(form.password.to_string())
).update_boxes(&*conn);
Redirect::to(uri!(super::session::new))
})
.map_err(|e| Template::render("users/new", json!({
"errors": e.inner(),
"form": form
})))
}
#[get("/@/<name>/outbox")]
fn outbox(name: String, conn: DbConn) -> ActivityStream<OrderedCollection> {
let user = User::find_local(&*conn, name).unwrap();
@ -210,13 +265,13 @@ fn inbox(name: String, conn: DbConn, data: String) -> String {
}
}
#[get("/@/<name>/followers", format = "application/activity+json")]
fn ap_followers(name: String, conn: DbConn) -> ActivityStream<OrderedCollection> {
#[get("/@/<name>/followers")]
fn ap_followers(name: String, conn: DbConn, _ap: ApRequest) -> ActivityStream<OrderedCollection> {
let user = User::find_local(&*conn, name).unwrap();
let followers = user.get_followers(&*conn).into_iter().map(|f| Id::new(f.ap_url)).collect::<Vec<Id>>();
let mut coll = OrderedCollection::default();
coll.object_props.set_id_string(format!("{}/followers", user.ap_url)).expect("Follower collection: id error");
coll.object_props.set_id_string(user.followers_endpoint).expect("Follower collection: id error");
coll.collection_props.set_total_items_u64(followers.len() as u64).expect("Follower collection: totalItems error");
coll.collection_props.set_items_link_vec(followers).expect("Follower collection: items error");
ActivityStream::new(coll)

View File

@ -17,7 +17,7 @@ fn nodeinfo() -> Content<String> {
}).to_string())
}
#[get("/.well-known/host-meta", format = "application/xml")]
#[get("/.well-known/host-meta")]
fn host_meta() -> String {
format!(r#"
<?xml version="1.0"?>

View File

@ -151,7 +151,13 @@ fn quick_setup(conn: DbConn) {
let instance = Instance::insert(&*conn, NewInstance {
public_domain: domain,
name: name,
local: true
local: true,
long_description: String::new(),
short_description: String::new(),
default_license: String::from("CC-0"),
open_registrations: true,
short_description_html: String::new(),
long_description_html: String::new()
});
println!("{}\n", " ✔️ Your instance was succesfully created!".green());

14
static/js/autoExpand.js Normal file
View File

@ -0,0 +1,14 @@
function autosize(){
const el = this;
el.style.height = 'auto';
el.style.height = (el.scrollHeight ) + 'px';
}
const articleContent = document.querySelector('#content');
let offset = 0;
let style = window.getComputedStyle(articleContent, null);
offset += parseInt(style['paddingTop']) + parseInt(style['paddingBottom']);
autosize.bind(articleContent)();
articleContent.addEventListener('keyup', autosize);

View File

@ -1,3 +1,5 @@
/* color palette: https://coolors.co/23f0c7-ef767a-7765e3-6457a6-ffe347 */
@import url('/static/fonts/Route159/Route159.css');
@import url('/static/fonts/Lora/Lora.css');
@import url('/static/fonts/Playfair_Display/PlayfairDisplay.css');
@ -20,6 +22,26 @@ a, a:visited {
outline: none;
}
small {
margin-left: 1em;
color: #242424;
opacity: 0.6;
font-size: 0.75em;
word-wrap: break-word;
word-break: break-all;
}
.center {
text-align: center;
font-weight: bold;
opacity: 0.6;
padding: 5em;
}
.spaced {
margin: 4rem 0;
}
/*
* == Header ==
*/
@ -126,7 +148,6 @@ main article {
font-family: "Lora", serif;
font-size: 1.2em;
line-height: 1.7em;
text-align: justify;
}
article img {
@ -135,7 +156,7 @@ article img {
/* Article.Meta */
main .article-meta {
main .article-meta, main .article-meta button {
padding: 0;
font-size: 1.1em;
margin-top: 10%;
@ -173,8 +194,8 @@ main .article-meta .reshares > p {
font-size: 1.5em;
}
main .article-meta .likes a.button,
main .article-meta .reshares a.button {
main .article-meta .likes button,
main .article-meta .reshares button {
display: flex;
flex-direction: column;
align-items: center;
@ -187,12 +208,12 @@ main .article-meta .reshares a.button {
}
main .article-meta .likes > p,
main .article-meta .likes a.button:hover { color: #E92F2F; }
main .article-meta .likes button:hover { color: #E92F2F; }
main .article-meta .reshares > p,
main .article-meta .reshares a.button:hover { color: #7765E3; }
main .article-meta .reshares button:hover { color: #7765E3; }
main .article-meta .likes a.button:before,
main .article-meta .reshares a.button:before {
main .article-meta .likes button i,
main .article-meta .reshares button i {
transition: background 0.1s ease-in;
display: flex;
align-items: center;
@ -203,42 +224,39 @@ main .article-meta .reshares a.button:before {
height: 2.5em;
border-radius: 50%;
font-family: "Font Awesome 5 Free";
}
main .article-meta .likes a.button:before {
content: "";
main .article-meta .likes button i {
color: #E92F2F;
border: solid #E92F2F thin;
font-weight: 400;
}
main .article-meta .likes a.button:hover:before {
main .article-meta .likes button:hover i {
background: rgba(233, 47, 47, 0.15);
}
main .article-meta .reshares a.button:before {
content: "";
main .article-meta .reshares button i {
color: #7765E3;
border: solid #7765E3 thin;
font-weight: 600;
}
main .article-meta .reshares a.button:hover:before {
main .article-meta .reshares button:hover i {
background: rgba(119, 101, 227, 0.15);
}
main .article-meta .likes a.button.liked:before { background: #E92F2F; }
main .article-meta .likes a.button.liked:hover:before {
main .article-meta .likes button.liked i { background: #E92F2F; }
main .article-meta .likes button.liked:hover i {
background: rgba(233, 47, 47, 0.25);
color: #E92F2F;
}
main .article-meta .reshares a.button.reshared:before { background: #7765E3; }
main .article-meta .reshares a.button.reshared:hover:before {
main .article-meta .reshares button.reshared i { background: #7765E3; }
main .article-meta .reshares button.reshared:hover i {
background: rgba(119, 101, 227, 0.25);
color: #7765E3;
}
main .article-meta .likes a.button.liked:before,
main .article-meta .reshares a.button.reshared:before {
main .article-meta .likes button.liked i,
main .article-meta .reshares button.reshared i {
color: #F4F4F4;
font-weight: 900;
}
@ -254,6 +272,11 @@ main .article-meta .comments > * { margin-left: 20%; margin-right: 20%; }
font-weight: 600;
}
/* New comment */
main .article-meta .comments form input[type="submit"]
{ font-size: 1em; }
/* Comment / Respond button */
main .article-meta .comments a.button:before {
@ -281,36 +304,35 @@ main .article-meta .comments > a.button { margin-bottom: 1em; }
main .article-meta .comments .list {
display: grid;
margin: 0;
padding: 0 20%;
background: #ECECEC;
}
/* ~ Comment ~ */
.comments .list .comment {
background: #ECECEC;
padding: 2em 20%;
padding: 2em;
font-size: 1em;
}
.comments .list > .comment {
border: none;
}
.comments .list .comment .author {
display: flex;
flex-direction: row;
align-items: center;
align-content: center;
}
.comments .list .comment .author * {
transition: all 0.1s ease-in;
}
.comments .list .comment .author .display-name {
transition: color 0.1s ease-in;
color: #242424;
}
.comments .list .comment .author:hover .display-name { color: #7765E3; }
.comments .list .comment .author .username {
transition: color 0.1s ease-in;
margin-left: 1em;
color: #777777;
font-size: 0.9em;
}
.comments .list .comment .author:hover .username { color: #444444; }
.comments .list .comment .author:hover small { opacity: 1; }
.comments .list .comment .text {
padding: 1.25em 0;
@ -326,54 +348,57 @@ main .article-meta .comments .list {
label {
display: block;
margin: 1em 0;
margin: 2em auto 1em;
font-size: 1.2em;
max-width: 40rem;
}
input {
input, textarea {
transition: all 0.1s ease-in;
display: block;
width: 100%;
margin: auto auto 5em;
padding: 0.5em;
max-width: 40rem;
margin: auto;
padding: 1em;
box-sizing: border-box;
background: #F4F4F4;
color: #242424;
border: none;
border-bottom: solid #DADADA 2px;
border: solid #DADADA thin;
border-radius: 0.5em;
font-size: 1.2em;
font-weight: 400;
}
form input[type="submit"] { margin: 2em auto; }
input:focus {
input:focus, textarea:focus {
background: #FAFAFA;
border-bottom-color: #7765E3;
border-color: #7765E3;
}
textarea {
display: block;
width: 100%;
min-height: 4em;
margin: auto;
padding: 1em;
box-sizing: border-box;
resize: vertical;
background: #ECECEC;
color: #242424;
border: none;
resize: vertical;
font-family: "Lora", serif;
font-size: 1.1em;
line-height: 1.5em;
}
font-family: "Lora", serif;
font-size: 1.1em;
line-height: 1.5em;
text-align: justify;
input[type="checkbox"] {
display: inline;
margin: initial;
min-width: initial;
width: initial;
}
/* Button & Submit */
.button, input[type="submit"] {
.button, input[type="submit"], button {
transition: all 0.1s ease-in;
display: inline-block;
border-radius: 0.5em;
margin: 0.5em 0;
margin: 0.5em auto;
padding: 0.75em 1em;
background: transparent;
@ -382,16 +407,27 @@ textarea {
cursor: pointer;
}
input[type="submit"] { display: block; }
.button:hover, input[type="submit"]:hover {
background: #7765E399;
color: white;
}
/* Errors */
p.error {
color: #ef767a;
font-weight: bold;
max-width: 40rem;
margin: 1em auto;
}
/*
* == New post ==
*/
form.new-post .title {
margin: 0 auto;
padding: 0.75em 0;
background: none;
@ -399,24 +435,23 @@ form.new-post .title {
font-family: "Playfair Display", serif;
font-size: 2em;
text-align: left;
}
form.new-post textarea {
min-height: 8em;
padding: 0;
background: none;
min-height: 20em;
overflow-y: hidden;
resize: none;
box-sizing: content-box;
}
form.new-post input[type="submit"] {
display: block;
margin: 1em auto;
width: 60%;
background: #DADADA;
background: #ECECEC;
color: #242424;
border: none;
font-family: "Playfair Display", serif;
font-size: 1.5em;
}
form.new-post input[type="submit"]:hover { background: #DADADA; }
/*
* == User ==
@ -462,11 +497,13 @@ form.new-post input[type="submit"] {
box-sizing: border-box;
background: #E3E3E3;
text-overflow: ellipsis;
}
.list .card {
/* TODO */
background: 0;
margin: 0;
margin: 2em 0;
padding: 0;
min-height: 0;
}
@ -496,6 +533,34 @@ form.new-post input[type="submit"] {
overflow: hidden;
}
/* Presentation */
.presentation > h2, .presentation > a {
text-align: center;
}
.presentation > a {
font-size: 1.2em;
margin: 1em;
}
/* Stats */
.stats {
display: flex;
justify-content: space-around;
margin: 2em;
}
.stats > div {
display: flex;
flex-direction: column;
text-align: center;
}
.stats em {
text-align: center;
font-weight: bold;
}
/* ================= *
* Small Screens *
* ================= */
@ -592,3 +657,46 @@ form.new-post input[type="submit"] {
min-height: 80%;
}
}
/*== Pagination ==*/
.pagination {
display: flex;
justify-content: space-evenly;
}
.pagination > * {
padding: 2em;
}
/*== Flex boxes ==*/
.flex {
display: flex;
flex-direction: row;
}
.flex .grow {
flex: 1;
}
.left-icon {
align-self: center;
padding: 1em;
background: #DADADA;
border-radius: 50px;
margin: 1em;
margin-right: 2em;
}
/*== Footer ==*/
body > footer {
display: flex;
align-content: center;
justify-content: space-between;
background: #ECECEC;
padding: 0 20%;
margin-top: 5em;
}
body > footer * {
margin: 5em 0;
}

View File

@ -37,5 +37,16 @@
{% block content %}
{% endblock content %}
</main>
<footer>
<span>Plume 0.1.0</span>
<a href="/about">{{ "About this instance" | _ }}</a>
<a href="https://github.com/Plume-org/Plume">{{ "Source code" | _ }}</a>
<a href="https://riot.im/app/#/room/#plume:disroot.org">{{ "Matrix room" | _ }}</a>
{% if account %}
{% if account.is_admin %}
<a href="/admin">{{ "Administration" | _ }}</a>
{% endif %}
{% endif %}
</footer>
</body>
</html>

View File

@ -5,22 +5,36 @@
{{ blog.title }}
{% endblock title %}
{% block header %}
<a href=".">{{ blog.title }}</a>
{% endblock header %}
{% block content %}
<h1>{{ blog.title }} (~{{ blog.actor_id }})</h1>
<h1>{{ blog.title }} <small>~{{ blog.fqn }}</small></h1>
<p>{{ blog.summary }}</p>
<p>
{{ "{{ count }} authors in this blog: " | _n(singular="One author in this blog: ", count = n_authors) }}
{% for author in authors %}
<a class="author" href="/@/{{ author.fqn }}">{{ author.name }}</a>{% if not loop.last %},{% endif %}
{% endfor %}
</p>
<p>
{{ "{{ count }} articles in this blog" | _n(singular="One article in this blog", count = n_articles) }}
</p>
<section>
<h2>{{ "Latest articles" | _ }}</h2>
{% if recents | length < 1 %}
{% if posts | length < 1 %}
<p>{{ "No posts to see here yet." | _ }}</p>
{% endif %}
{% if is_author %}
<a href="new" class="button inline-block">{{ "New article" | _ }}</a>
<a href="/~/{{ blog.fqn }}/new/" class="button inline-block">{{ "New article" | _ }}</a>
{% endif %}
<div class="cards">
{% for article in recents %}
{% for article in posts %}
{{ macros::post_card(article=article) }}
{% endfor %}
</div>
{{ macros::paginate(page=page, total=n_pages) }}
</section>
{% endblock content %}

View File

@ -1,4 +1,5 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ "New blog" | _ }}
@ -7,8 +8,8 @@
{% block content %}
<h1>{{ "Create a blog" | _ }}</h1>
<form method="post">
<label for="title">{{ "Title" | _ }}</label>
<input type="text" id="title" name="title" />
{{ macros::input(name="title", label="Title", errors=errors, form=form, props='required minlength="1"') }}
<input type="submit" value="{{ "Create blog" | _ }}"/>
</form>
{% endblock content %}

View File

@ -0,0 +1,34 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
Administration of {{ instance.name }}
{% endblock title %}
{% block content %}
<h1>{{ "Administration" | _ }}</h1>
<h2>{{ "Instance settings" | _ }}</h2>
<form method="post">
{{ macros::input(name="name", label="Name", errors=errors, form=form, props='minlenght="1"', default=instance) }}
<label for="open_registrations">
{% if instance.open_registrations %}
<input type="checkbox" name="open_registrations" id="open_registrations" checked>
{% else %}
<input type="checkbox" name="open_registrations" id="open_registrations">
{% endif %}
{{ "Allow anyone to register" | _ }}
</label>
<label for="short_description">{{ "Short description" | _ }}<small>{{ "Markdown is supported" | _ }}</small></label>
<textarea id="short_description" name="short_description">{{ form.short_description | default(value=instance.short_description) }}</textarea>
<label for="long_description">{{ "Long description" | _ }}<small>{{ "Markdown is supported" | _ }}</small></label>
<textarea id="long_description" name="long_description">{{ form.long_description | default(value=instance.long_description) }}</textarea>
{{ macros::input(name="default_license", label="Default license", errors=errors, form=form, props='minlenght="1"', default=instance) }}
<input type="submit" value="{{ "Save settings" | _ }}"/>
</form>
{% endblock content %}

View File

@ -1,15 +0,0 @@
{% extends "base" %}
{% block title %}
{{ "Configuration" | _ }}
{% endblock title %}
{% block content %}
<h1>{{ "Configure your instance" | _ }}</h1>
<form method="post">
<label for="name">{{ "Name" | _ }}</label>
<input type="text" id="name" name="name" />
<input type="submit" value="{{ "Let's go!" | _ }}" />
</form>
{% endblock content %}

View File

@ -14,4 +14,38 @@
{{ macros::post_card(article=article) }}
{% endfor %}
</div>
{{ macros::paginate(page=page, total=n_pages) }}
<section class="spaced">
<div class="cards">
<div class="presentation card">
<h2>{{ "What is Plume?" | _ }}</h2>
<main>
<p>{{ "Plume is a decentralized blogging engine." | _ }}</p>
<p>{{ "Authors can manage various blogs from an unique website." | _ }}</p>
<p>{{ "Articles are also visible on other Plume websites, and you can interact with them directly from other platforms like Mastodon." | _ }}</p>
</main>
<a href="/users/new">{{ "Create your account" | _ }}</a>
</div>
<div class="presentation card">
<h2>{{ "About {{ instance_name }}" | _(instance_name=instance.name) }}</h2>
<main>
{{ instance.short_description_html | safe }}
<section class="stats">
<div>
<p>{{ "Home to" | _ }}</p>
<em>{{ n_users }}</em>
<p>{{ "people" | _ }}</p>
</div>
<div>
<p>{{ "Who wrote" | _ }}</p>
<em>{{ n_articles }}</em>
<p>{{ "articles" | _ }}</p>
</div>
</section>
</main>
<a href="/about">{{ "Read the detailed rules" | _ }}</a>
</div>
</div>
</section>
{% endblock content %}

View File

@ -1,9 +1,4 @@
{% macro post_card(article) %}
{% if article.author.display_name %}
{% set name = article.author.display_name %}
{% else %}
{% set name = article.author.username %}
{% endif %}
<div class="card">
<h3><a href="{{ article.url }}">{{ article.post.title }}</a></h3>
<main
@ -14,10 +9,50 @@
link_1='<a href="/@/',
link_2=article.author.fqn,
link_3='/">',
name=name,
name=article.author.name,
link_4="</a>")
}}
⋅ {{ article.date | date(format="%B %e") }}
⋅ <a href="/~/{{ article.blog.fqn }}/">{{ article.blog.title }}</a>
</p>
</div>
{% endmacro post_card %}
{% macro input(name, label, errors, form, type="text", props="", optional=false, default='', details='') %}
<label for="{{ name }}">
{{ label | _ }}
{% if optional %}
<small>{{ "Optional" | _ }}</small>
{% endif %}
<small>{{ details }}</small>
</label>
{% if errors is defined and errors[name] %}
{% for err in errors[name] %}
<p class="error">{{ err.message | default(value="Unknown error") | _ }}</p>
{% endfor %}
{% endif %}
{% set default = default[name] | default(value="") %}
<input type="{{ type }}" id="{{ name }}" name="{{ name }}" value="{{ form[name] | default(value=default) }}" {{ props | safe }}/>
{% endmacro input %}
{% macro paginate(page, total, previous="Previous page", next="Next page") %}
<div class="pagination">
{% if page != 1 %}
<a href="?page={{ page - 1 }}">{{ previous | _ }}</a>
{% endif %}
{% if page < total %}
<a href="?page={{ page + 1 }}">{{ next | _ }}</a>
{% endif %}
</div>
{% endmacro %}
{% macro comment(comm) %}
<div class="comment" id="comment-{{ comm.id }}">
<a class="author" href="/@/{{ comm.author.fqn }}/">
<span class="display-name">{{ comm.author.name }}</span>
<small>@{{ comm.author.username }}</small>
</a>
<div class="text">{{ comm.content | safe }}</div>
<a class="button" href="?responding_to={{ comm.id }}">{{ "Respond" | _ }}</a>
{% for res in comm.responses %}
{{ self::comment(comm=res) }}
{% endfor %}
</div>
{% endmacro %}

View File

@ -1,4 +1,5 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ "Notifications" | _ }}
@ -8,12 +9,58 @@
<h1>{{ "Notifications" | _ }}</h1>
<div class="list">
{% for notification in notifications %}
<div class="card">
<h3><a href="{% if notification.link %}{{ notification.link }}/{% else %}#{% endif %}">{{ notification.title | _(data=notification.data) }}</h3>
{% if notification.content %}
<p>{{ notification.content }}</p>
<div class="card flex">
{% if notification.kind == "COMMENT" %}
<i class="fa fa-comment left-icon"></i>
<main class="grow">
<h3><a href="{{ notification.object.post.url }}#comment-{{ notification.object.id }}">
{{ "{{ user }} commented your article." | _(user=notification.object.user.name | escape) }}
</a></h3>
<p><a href="{{ notification.object.post.url }}">{{ notification.object.post.post.title }}</a></p>
</main>
<p><small>{{ notification.creation_date | date(format="%B %e, %H:%M") }}</small></p>
{% elif notification.kind == "FOLLOW" %}
<i class="fa fa-user-plus left-icon"></i>
<main class="grow">
<h3><a href="/@/{{ notification.object.follower.fqn }}/">
{{ "{{ user }} is now following you." | _(user=notification.object.follower.name | escape) }}
</a></h3>
</main>
<p><small>{{ notification.creation_date | date(format="%B %e, %H:%M") }}</small></p>
{% elif notification.kind == "LIKE" %}
<i class="fa fa-heart left-icon"></i>
<main class="grow">
<h3>
{{ "{{ user }} liked your article." | _(user=notification.object.user.name | escape) }}
</h3>
<p><a href="{{ notification.object.post.url }}">{{ notification.object.post.post.title }}</a></p>
</main>
<p><small>{{ notification.creation_date | date(format="%B %e, %H:%M") }}</small></p>
{% elif notification.kind == "MENTION" %}
<i class="fa fa-at left-icon"></i>
<main class="grow">
<h3><a href="{{ notification.object.url }}">
{{ "{{ user }} mentioned you." | _(user=notification.object.user.name | escape) }}
</a></h3>
</main>
<p><small>{{ notification.creation_date | date(format="%B %e, %H:%M") }}</small></p>
{% elif notification.kind == "RESHARE" %}
<i class="fa fa-retweet left-icon"></i>
<main class="grow">
<h3>
{{ "{{ user }} reshared your article." | _(user=notification.object.user.name | escape) }}
</h3>
<p><a href="{{ notification.object.post.url }}">{{ notification.object.post.post.title }}</a></p>
</main>
<p><small>{{ notification.creation_date | date(format="%B %e, %H:%M") }}</small></p>
{% endif %}
</div>
{% endfor %}
</div>
{{ macros::paginate(page=page, total=n_pages) }}
{% endblock content %}

View File

@ -1,28 +1,22 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ post.title }}
{{ article.post.title }}
{% endblock title %}
{% block header %}
<a href="../">{{ blog.title }}</a>
<a href="/~/{{ blog.fqn }}">{{ blog.title }}</a>
{% endblock header %}
{% block content %}
<h1 class="article">{{ post.title }}</h1>
<h1 class="article">{{ article.post.title }}</h1>
<p class="article-info">
{% if author.display_name %}
{% set name = author.display_name %}
{% else %}
{% set name = author.username %}
{% endif %}
<span class="author">{{ "Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}" | _(
link_1='<a href="/@/',
url=author.fqn,
link_2='/">',
name=name,
name=author.name,
link_3="</a>"
)
}}</a></span>
@ -30,38 +24,42 @@
<span class="date">{{ date | date(format="%B %e, %Y") }}</span>
</p>
<article>
{{ post.content | safe }}
{{ article.post.content | safe }}
</article>
<div class="article-meta">
<p>{{ "This article is under the {{ license }} license." | _(license=post.license) }}</p>
<p>{{ "This article is under the {{ license }} license." | _(license=article.post.license) }}</p>
<div class="actions">
<div class="likes">
<p aria-label="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}" title="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}">{{ n_likes }}</p>
{% if account %}
<div class="actions">
<form class="likes" action="{{ article.url }}like" method="POST">
<p aria-label="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}" title="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}">{{ n_likes }}</p>
{% if has_liked %}
<a class="button liked" href="like">{{ "I don't like this anymore" | _ }}</a>
{% else %}
<a class="button" href="like">{{ "Add yours" | _ }}</a>
{% endif %}
{% if has_liked %}
<button type="submit" class="liked"><i class="far fa-heart"></i>{{ "I don't like this anymore" | _ }}</button>
{% else %}
<button type="submit"><i class="fa fa-heart"></i>{{ "Add yours" | _ }}</button>
{% endif %}
</form>
<form class="reshares" action="{{ article.url }}reshare" method="POST">
<p aria-label="{{ "{{ count }} reshares" | _n(singular="One reshare", count=n_reshares) }}" title="{{ "{{ count }} reshares" | _n(singular="One reshare", count=n_reshares) }}">{{ n_reshares }}</p>
{% if has_reshared %}
<button type="submit" class="reshared"><i class="far fa-retweet"></i>{{ "I don't want to reshare this anymore" | _ }}</button>
{% else %}
<button type="submit"><i class="fa fa-retweet"></i>{{ "Reshare" | _ }}</button>
{% endif %}
</form>
</div>
<div class="reshares">
<p aria-label="{{ "{{ count }} reshares" | _n(singular="One reshare", count=n_reshares) }}" title="{{ "{{ count }} reshares" | _n(singular="One reshare", count=n_reshares) }}">{{ n_reshares }}</p>
{% if has_reshared %}
<a class="button reshared" href="reshare">{{ "I don't want to reshare this anymore" | _ }}</a>
{% else %}
<a class="button" href="reshare">{{ "Reshare" | _ }}</a>
{% endif %}
</div>
</div>
{% else %}
<p class="center">{{ "Login or use your Fediverse account to interact with this article" | _ }}</p>
{% endif %}
<div class="comments">
<h2>{{ "Comments" | _ }}</h2>
{% if account %}
<form method="post" action="/~/{{ blog.actor_id }}/{{ post.slug }}/comment">
<form method="post" action="{{ article.url }}comment">
<label for="content">{{ "Your comment" | _ }}</label>
{% if previous %}
<input type="hidden" name="responding_to" value="{{ previous.id }}"/>
@ -72,24 +70,15 @@
</form>
{% endif %}
<div class="list">
{% for comment in comments %}
{% if comment.author.display_name %}
{% set comment_author_name = comment.author.display_name %}
{% else %}
{% set comment_author_name = comment.author.username %}
{% endif %}
<div class="comment" id="comment-{{ comment.id }}">
<a class="author" href="{{ comment.author.ap_url }}">
<span class="display-name">{{ comment.author.display_name }}</span>
<span class="username">@{{ comment.author.username }}</span>
</a>
<div class="text">{{ comment.content | safe }}</div>
<a class="button" href="?responding_to={{ comment.id }}">{{ "Respond" | _ }}</a>
</div>
{% endfor %}
</div>
{% if comments | length > 0 %}
<div class="list">
{% for comment in comments %}
{{ macros::comment(comm=comment) }}
{% endfor %}
</div>
{% else %}
<p class="center">{{ "No comments yet. Be the first to react!" | _ }}</p>
{% endif %}
</div>
</div>
{% endblock content %}

View File

@ -1,4 +1,5 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ "New post" | _ }}
@ -7,12 +8,21 @@
{% block content %}
<h1>{{ "Create a post" | _ }}</h1>
<form class="new-post" method="post">
<input type="text" class="title" name="title" placeholder="{{ "Title" | _ }}">
<textarea name="content" placeholder="{{ "Content" | _ }}"></textarea>
{{ macros::input(name="title", label="Title", errors=errors, form=form, props="required") }}
<label for="license">{{ "License" | _ }}</label>
<input type="text" id="licence" name="license" />
{% if errors is defined and errors.content %}
{% for err in errors.content %}
<p class="error">{{ err.message | default(value="Unknown error") | _ }}</p>
{% endfor %}
{% endif %}
<label for="content">{{ "Content" | _ }}<small>{{ "Markdown is supported" | _ }}</small></label>
<textarea id="content" name="content" value="{{ form.content | default(value="") }}" rows="20"></textarea>
{% set license_infos = "Default license will be {{ instance.default_license }}" | _(instance=instance) %}
{{ macros::input(name="license", label="License", errors=errors, form=form, optional=true, details=license_infos) }}
<input type="submit" value="{{ "Publish" | _ }}" />
</form>
<script src="/static/js/autoExpand.js"></script>
{% endblock content %}

View File

@ -1,4 +1,5 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ "Login" | _ }}
@ -10,11 +11,8 @@
<p>{{ message }}</p>
{% endif %}
<form method="post">
<label for="email_or_name">{{ "Username or email" | _ }}</label>
<input type="text" id="email_or_name" name="email_or_name" />
<label for="password">{{ "Password" | _ }}</label>
<input type="password" id="password" name="password" />
{{ macros::input(name="email_or_name", label="Username or email", errors=errors, form=form, props='minlenght="1"') }}
{{ macros::input(name="password", label="Password", errors=errors, form=form, type="password", props='minlenght="1"') }}
<input type="submit" value="{{ "Login" | _ }}" />
</form>

View File

@ -13,13 +13,13 @@
{% if blogs | length < 1 %}
<p>{{ "You don't have any blog yet. Create your own, or ask to join one." | _ }}</p>
{% endif %}
<a class="button" href="/blogs/new">{{ "Start a new blog" | _ }}</a>
<div class="list">
<div class="cards">
{% for blog in blogs %}
<div class="card">
<h3><a href="/~/{{ blog.actor_id }}/">{{ blog.title }}</a></h3>
</div>
{% endfor %}
</div>
<a class="button" href="/blogs/new">{{ "Start a new blog" | _ }}</a>
</section>
{% endblock content %}

View File

@ -1,13 +1,8 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{% if user.display_name %}
{% set name = user.display_name %}
{% else %}
{% set name = user.username %}
{% endif %}
{{ "{{ name | escape }}'s followers" | _(name=name) }}
{{ "{{ name | escape }}'s followers" | _(name=user.name) }}
{% endblock title %}
{% block content %}
@ -16,15 +11,11 @@
<h2>{{ "Followers" | _ }}</h2>
<div class="cards">
{% for follower in followers %}
{% if follower.display_name %}
{% set follower_name = follower.display_name %}
{% else %}
{% set follower_name = follower.username %}
{% endif %}
<div class="card">
<h3><a href="/@/{{ follower.fqn }}/">{{ follower_name }}</a> &mdash; @{{ follower.fqn }}</h3>
<h3><a href="/@/{{ follower.fqn }}/">{{ follower.name }}</a> <small>@{{ follower.fqn }}</small></h3>
<main><p>{{ follower.summary | safe }}</p></main>
</div>
{% endfor %}
</div>
{{ macros::paginate(page=page, total=n_pages) }}
{% endblock content %}

View File

@ -1,6 +1,9 @@
<div class="user">
<h1>
{{ name }}
{{ user.name }}
<small>@{{ user.fqn }}</small>
{% if user.is_admin %}
<span class="badge">{{ "Admin" | _ }}</span>
{% endif %}
@ -15,18 +18,19 @@
{% endif %}
{% if is_remote %}
<a class="inline-block button" href="{{ user.ap_url }}">{{ "Open on {{ instance_url }}" | _(instance_url=instance_url) }}</a>
<a class="inline-block button" href="{{ user.ap_url }}" target="_blank">{{ "Open on {{ instance_url }}" | _(instance_url=instance_url) }}</a>
{% endif %}
{% if not is_self and account %}
{% set not_self = not is_self %}
{% if not_self and (account is defined) %}
{% if follows %}
<a href="follow/" class="inline-block button">{{ "Follow" | _ }}</a>
<a href="/@/{{ user.fqn }}/follow/" class="inline-block button">{{ "Unfollow" | _ }}</a>
{% else %}
<a href="follow/" class="inline-block button">{{ "Unfollow" | _ }}</a>
<a href="/@/{{ user.fqn }}/follow/" class="inline-block button">{{ "Follow" | _ }}</a>
{% endif %}
{% endif %}
</div>
<div>
<a href="followers/">{{ "{{ count }} followers" | _n(singular="One follower", count=n_followers) }}</a>
<a href="/@/{{ user.fqn }}/followers/">{{ "{{ count }} followers" | _n(singular="One follower", count=n_followers) }}</a>
</div>

View File

@ -1,4 +1,5 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{{ "New Account" | _ }}
@ -7,17 +8,10 @@
{% block content %}
<h1>{{ "Create an account" | _ }}</h1>
<form method="post">
<label for="username">{{ "Username" | _ }}</label>
<input type="text" id="username" name="username" />
<label for="email">{{ "Email" | _ }}</label>
<input type="email" id="email" name="email" />
<label for="password">{{ "Password" | _ }}</label>
<input type="password" id="password" name="password" />
<label for="password_confirmation">{{ "Password confirmation" | _ }}</label>
<input type="password" id="password_confirmation" name="password_confirmation" />
{{ macros::input(name="username", label="Username", errors=errors, form=form, props='minlenght="1"') }}
{{ macros::input(name="email", label="Email", errors=errors, form=form, type="email") }}
{{ macros::input(name="password", label="Password", errors=errors, form=form, type="password", props='minlenght="8"') }}
{{ macros::input(name="password_confirmation", label="Password confirmation", errors=errors, form=form, type="password", props='minlenght="8"') }}
<input type="submit" value="{{ "Create account" | _ }}" />
</form>