Compare commits

..

224 Commits

Author SHA1 Message Date
KitaitiMakoto ec3205b372 Merge pull request 'v0.7.1' (#1006) from v0.7.1 into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1006
2022-01-12 02:10:48 +00:00
Kitaiti Makoto 45119d9a8c (cargo-release) version {{version}} 2022-01-12 10:42:45 +09:00
Kitaiti Makoto 1065078f75 Update translation files 2022-01-12 10:29:50 +09:00
Kitaiti Makoto 0ce904a985 Update translation files 2022-01-12 10:24:14 +09:00
Kitaiti Makoto 254eec8e6a Follow cargo-release update 2022-01-12 10:19:58 +09:00
KitaitiMakoto 0e4cb4f6e1 Merge pull request '[skip ci]Fix # of pull reuqest in changelog' (#1004) from fix-changelog into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1004
2022-01-12 00:37:19 +00:00
Kitaiti Makoto 9b05ac90df [skip ci]Fix # of pull reuqest in changelog 2022-01-12 09:36:32 +09:00
KitaitiMakoto f28a7fa508 Merge pull request 'Add changelogs' (#1003) from changelog into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1003
2022-01-12 00:35:26 +00:00
Kitaiti Makoto 65e95d8998 Add changelogs 2022-01-12 09:34:39 +09:00
KitaitiMakoto 808b8f8e98 Merge pull request 'Fix #1001 Deny access to disabled sign-up strategy' (#1002) from restrict-signup into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1002
2022-01-12 00:33:04 +00:00
Kitaiti Makoto 43b46a8be4 Make email_signups::create return ErrorPage on error 2022-01-12 09:20:20 +09:00
Kitaiti Makoto 9bbfc71fc8 Fix registration openess condition mistake 2022-01-12 09:19:48 +09:00
Kitaiti Makoto 5d58b31f1c Remove unreachable code 2022-01-12 09:00:04 +09:00
Kitaiti Makoto e31a2238fb Respond with error status code when error 2022-01-12 08:58:42 +09:00
Kitaiti Makoto 7de37bc9b7 Hide password sign-up routings when it's disabled 2022-01-12 08:46:11 +09:00
Kitaiti Makoto 13f7734751 Hide email sign-up routings when it's disabled 2022-01-12 08:40:20 +09:00
Kitaiti Makoto b4395bce99 Implement request guard to detect enabled sign-up strategy 2022-01-12 08:39:33 +09:00
Kitaiti Makoto 7c82b08615 Use into() instead of explicitly wrapping return values 2022-01-12 04:36:01 +09:00
Kitaiti Makoto 6498dbfbb7 Reuse form values 2022-01-12 04:28:02 +09:00
Kitaiti Makoto 74254aed4a Move require_logins from plume-common to plume 2022-01-12 04:18:13 +09:00
KitaitiMakoto 8c48abf48e Merge pull request 'Update crates' (#997) from update-crates into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/997
2022-01-07 18:58:18 +00:00
Kitaiti Makoto 8958226604 Upgrade shrinkwraprs 2022-01-08 03:54:38 +09:00
Kitaiti Makoto 005a6db230 Update crates 2022-01-08 03:20:22 +09:00
KitaitiMakoto 4397abd8ab Merge pull request 'Add plume-front.pot' (#994) from front-po into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/994
2022-01-07 15:40:31 +00:00
Kitaiti Makoto e53882f555 Add plume-front.pot 2022-01-08 00:39:17 +09:00
KitaitiMakoto 5d5e61dfa1 Merge pull request 'Update crates' (#993) from update-crates into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/993
2022-01-06 21:45:53 +00:00
Kitaiti Makoto c5c6b70a89 Upgrade ldap3 2022-01-07 06:17:58 +09:00
Kitaiti Makoto 6778a0e943 Remove hyper from plume-common 2022-01-07 06:16:00 +09:00
Kitaiti Makoto 677e238c6d Follow API change of heck 2022-01-07 06:12:15 +09:00
Kitaiti Makoto b0bc2372fa Upgrade heck 2022-01-07 06:12:03 +09:00
Kitaiti Makoto 6a808c7cc5 Upgrade hex 2022-01-07 06:07:07 +09:00
Kitaiti Makoto d53543ccb1 Upgrade base64 2022-01-07 06:06:49 +09:00
Kitaiti Makoto 88d7d54601 Upgrade whatlang 2022-01-07 06:05:20 +09:00
Kitaiti Makoto 0f0c896887 Upgrade itertools 2022-01-07 06:05:05 +09:00
Kitaiti Makoto 65233c0a9a Upgrade ammonia 2022-01-07 05:53:09 +09:00
Kitaiti Makoto 32e1e4788f Upgrade shrinkwraprs 2022-01-07 05:49:06 +09:00
Kitaiti Makoto 181a78876b Remove askama_escape from dependencies of plume-models 2022-01-07 05:47:46 +09:00
Kitaiti Makoto 61d5446113 Use plume_common::escape() instead of askama_escape::escape() directly 2022-01-07 05:47:08 +09:00
Kitaiti Makoto c786569171 Define plume_common::escape() 2022-01-07 05:36:39 +09:00
Kitaiti Makoto d83a75e3f4 Add askama_escape to plume-common 2022-01-07 05:26:32 +09:00
Kitaiti Makoto a6f06559ea Remove rspassword from dependencies 2022-01-07 05:15:07 +09:00
Kitaiti Makoto 2084145dd3 Upgrade multipart 2022-01-07 05:12:29 +09:00
Kitaiti Makoto dd54058516 Upgrade guid-create 2022-01-07 05:09:08 +09:00
Kitaiti Makoto 4056a54d44 Upgrade tracing-subscriber 2022-01-07 05:03:07 +09:00
Kitaiti Makoto 191cd11741 Upgrade dotenv 2022-01-07 04:59:03 +09:00
Kitaiti Makoto 800e74da67 Follow API change of validator 2022-01-07 04:55:49 +09:00
Kitaiti Makoto 237da47950 Upgrade validator 2022-01-07 04:55:41 +09:00
Kitaiti Makoto ec12539fd0 Follow rsass API change 2022-01-07 04:41:20 +09:00
Kitaiti Makoto a537db559b Upgrade rsass 2022-01-07 04:41:11 +09:00
Kitaiti Makoto 2ba158df67 Update crates 2022-01-07 01:41:22 +09:00
KitaitiMakoto c0c066547f Merge pull request 'Update po files' (#991) from po into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/991
2022-01-06 13:24:09 +00:00
Kitaiti Makoto c3f59b14b9 Update po files 2022-01-06 22:14:24 +09:00
KitaitiMakoto 1d06a8f1ad Merge pull request 'Fixes #636 Email sign up feature' (#990) from mail-confirmation into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/990
2022-01-06 12:04:24 +00:00
Kitaiti Makoto efaf1295e9 Suppress clippy 2022-01-06 20:43:40 +09:00
Kitaiti Makoto 4bc9cf3ad1 [skip ci]Complete changelogs 2022-01-06 20:35:34 +09:00
Kitaiti Makoto 1e3851ea69 Execute SQLs for email_signups in transaction 2022-01-06 20:27:55 +09:00
Kitaiti Makoto b6d38536e3 Add email signup feature 2022-01-06 20:18:20 +09:00
Kitaiti Makoto 9b4c678aa9 Make signup token transparent 2022-01-06 18:40:24 +09:00
Kitaiti Makoto a65775d85b Implement EmailSignup 2022-01-05 03:17:58 +09:00
Kitaiti Makoto 192c7677c3 Run migration
% diesel migration run
2022-01-05 00:01:36 +09:00
Kitaiti Makoto 2a31a7b601 Define email_singups table 2022-01-05 00:01:36 +09:00
Kitaiti Makoto 355fd7cb1d Generate create_email_signups_table migration
% diesel migration generate create_email_signups_table
2022-01-05 00:01:36 +09:00
Kitaiti Makoto 40efd73dfc Add config for sign up strategy 2022-01-05 00:01:32 +09:00
KitaitiMakoto 31b144c76d Merge pull request 'Remove unnecessary prefix' (#986) from fix-tag into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/986
2022-01-04 10:17:53 +00:00
Kitaiti Makoto 31a46514cb Remove unnecessary prefix 2022-01-04 19:17:04 +09:00
KitaitiMakoto c8d906eb99 Merge pull request 'Quote version tag' (#985) from fix-action into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/985
2022-01-04 10:11:19 +00:00
Kitaiti Makoto 2895a1c819 Quote version tag 2022-01-04 19:10:37 +09:00
KitaitiMakoto a4a5d08662 Merge pull request 'deploy-tags' (#984) from deploy-tags into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/984
2022-01-04 10:08:31 +00:00
Kitaiti Makoto b97c9d2165 Deploy Docker images with tags 2022-01-04 19:07:47 +09:00
Kitaiti Makoto 5b7e8a69a5 Revert "Deploy tags"
This reverts commit d9a59f1b07.
2022-01-04 19:04:58 +09:00
KitaitiMakoto 9601e99e33 Merge pull request 'Deploy tags' (#983) from deploy-tags into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/983
2022-01-04 09:42:19 +00:00
Kitaiti Makoto d9a59f1b07 Deploy tags 2022-01-04 18:41:06 +09:00
KitaitiMakoto b6a6af906a Merge pull request '[skip ci]Add changelog abourt MAIL_PORT' (#982) from changelog into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/982
2022-01-03 16:07:13 +00:00
Kitaiti Makoto 2c4799ce27 [skip ci]Add changelog abourt MAIL_PORT 2022-01-04 01:06:12 +09:00
KitaitiMakoto b33b19849c Merge pull request 'Fix notification page error' (#981) from fix-notification-page into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/981
2022-01-03 14:29:43 +00:00
Kitaiti Makoto e398f36c57 Add changelog 2022-01-03 23:28:56 +09:00
Kitaiti Makoto ee6064eee8 Don't unwrap() 2022-01-03 23:28:05 +09:00
KitaitiMakoto 9d012c8f3c Merge pull request 'Closes #944 Mail server port configuration' (#980) from mail-server-port into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/980
2022-01-03 10:37:53 +00:00
Kitaiti Makoto 8888dbba0a Initialize SMTP client with port number 2022-01-03 18:09:26 +09:00
Kitaiti Makoto 6f8d5c1eb4 Add SmtpClient::new_with_addr() method 2022-01-03 18:09:13 +09:00
Kitaiti Makoto 62da4a3d5c Add native-tls to plume-models' dependencies 2022-01-03 18:04:49 +09:00
Kitaiti Makoto 5cfc8e71a5 Remove Lettre from plume module dependencies 2022-01-03 17:36:37 +09:00
Kitaiti Makoto a599760891 Use smtp module from plume_models instead of lettre directly 2022-01-03 17:36:11 +09:00
Kitaiti Makoto 00324f668f Add port field to MailConfig 2022-01-03 17:25:46 +09:00
Kitaiti Makoto d4549704b9 Install Lettre 2022-01-03 17:11:35 +09:00
Kitaiti Makoto 0836e3d693 Add Lettre to plume-models' dependencies 2022-01-03 17:09:47 +09:00
Kitaiti Makoto 0058c3053d Move mail config from plume::mail::mailer to plume_models::CONFIG 2022-01-03 15:50:04 +09:00
KitaitiMakoto 2a1a0a23a5 Merge pull request 'Move bottombar styles to _article.scss' (#978) from bottombar-styling into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/978
2022-01-02 18:03:46 +00:00
Kitaiti Makoto 5614e3bd59 Move bottombar styles to _article.scss 2022-01-03 03:01:59 +09:00
KitaitiMakoto acbda3cde1 Merge pull request 'Make bottom bar smaller in narrow window' (#977) from layout-post-control into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/977
2022-01-02 17:50:20 +00:00
Kitaiti Makoto 0755436458 Make bottom bar smaller in narrow window 2022-01-03 02:49:45 +09:00
KitaitiMakoto 3daf405ae2 Merge pull request 'Make blog cover clickable' (#976) from clickable-blog-image into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/976
2022-01-02 17:40:30 +00:00
Kitaiti Makoto 53dc3b0c03 Fix cover size of posts 2022-01-03 02:39:17 +09:00
Kitaiti Makoto 371dcc5091 Address blog title positoin in dashboard 2022-01-03 02:37:45 +09:00
Kitaiti Makoto 905fe54fa3 Make blog cover a link 2022-01-03 02:35:07 +09:00
Kitaiti Makoto 62c0827ff5 Remove needless whitespaces 2022-01-03 02:32:17 +09:00
KitaitiMakoto d6c65ce81a Merge pull request 'Fix #927 Ensure Post ap_url' (#975) from ensure-ap-url into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/975
2022-01-02 17:24:04 +00:00
Kitaiti Makoto 5532b4a4d7 Ensure Post ap_url 2022-01-03 02:21:34 +09:00
KitaitiMakoto 637bd3347b Merge pull request 'Fix #967 Fix comment link' (#974) from fix-comment-link into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/974
2022-01-02 17:08:10 +00:00
Kitaiti Makoto bac373a818 Fix comment link 2022-01-03 02:06:59 +09:00
Kitaiti Makoto f0e7ea5640 (cargo-release) version {{version}} 2022-01-03 01:17:07 +09:00
Kitaiti Makoto 4b981e0fad (cargo-release) version {{version}} 2022-01-03 00:56:29 +09:00
KitaitiMakoto 8fb9d861de Merge pull request '[skip ci]Give up to deploy tags' (#973) from giveup-tag into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/973
2022-01-02 15:43:05 +00:00
Kitaiti Makoto 199269ba3c [skip ci]Give up to deploy tags 2022-01-03 00:42:36 +09:00
KitaitiMakoto 0b9ec4c52c Merge pull request 'Use Git tag for Docker image tag' (#972) from deploy-tag into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/972
2022-01-02 15:14:38 +00:00
Kitaiti Makoto 0e51565cc8 Use Git tag for Docker image tag 2022-01-03 00:14:00 +09:00
KitaitiMakoto 0418d35b67 Merge pull request 'Fix a typo' (#971) from deploy-tag into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/971
2022-01-02 15:06:26 +00:00
Kitaiti Makoto bf9d25363b Fix a typo 2022-01-03 00:04:57 +09:00
KitaitiMakoto 84f00c57d1 Merge pull request 'Fix tag calculation' (#970) from deploy-tag into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/970
2022-01-02 15:02:15 +00:00
Kitaiti Makoto 7c1a5421fa Fix tag calculation 2022-01-03 00:01:18 +09:00
KitaitiMakoto 3815bfe980 Merge pull request '[skip ci]Deploy tags to Docker Hub' (#969) from deploy-tag into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/969
2022-01-02 14:51:03 +00:00
Kitaiti Makoto de448c3192 [skip ci]Deploy tags to Docker Hub 2022-01-02 23:49:42 +09:00
KitaitiMakoto 967e2dfde6 Merge pull request 'Add GitHub Action to deploy Docker image' (#968) from gh-action into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/968
2022-01-02 14:28:27 +00:00
Kitaiti Makoto dd3c1eac5f Add GitHub Action to deploy Docker image 2022-01-02 23:26:39 +09:00
KitaitiMakoto abc0a794c1 Merge pull request 'v0.7.0' (#923) from v0.7.0 into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/923
2022-01-02 11:48:46 +00:00
Kitaiti Makoto ef628aa498 (cargo-release) version {{version}} 2022-01-02 20:41:28 +09:00
Kitaiti Makoto 011bd9602d Update plume-buildenv Docker image to v0.4.0 2021-12-12 00:54:26 +09:00
Kitaiti Makoto b0745cfd82 Fix search string in release.toml 2021-12-12 00:38:22 +09:00
Kitaiti Makoto d1b1d9f507 Update translations 2021-12-12 00:01:11 +09:00
Kitaiti Makoto 742d545f1c Update po files 2021-12-11 23:57:05 +09:00
Kitaiti Makoto 67dec4df22 Merge remote-tracking branch 'origin/main' into v0.7.0 2021-12-09 14:35:10 +09:00
KitaitiMakoto 33f3bbb774 Merge pull request 'Proper scaling of the default image, when a user doesn't have custom avatar' (#965) from mareklach/Plume:proper_scaling_for_blank_user_avatar into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/965
2021-12-09 05:33:52 +00:00
mareklach 7c1df80695 Update 'assets/themes/default/_global.scss' 2021-12-06 15:15:28 +00:00
mareklach 2adbb6f74c Merge branch 'main' into proper_scaling_for_blank_user_avatar 2021-12-06 14:52:28 +00:00
mareklach 79c05e8381 Proper scaling of the default image, when a user doesn't have custom avatar 2021-12-06 14:48:05 +00:00
Kitaiti Makoto 31a1591043 Merge remote-tracking branch 'origin/main' into v0.7.0 2021-12-06 07:32:05 +09:00
KitaitiMakoto 0ede2ab3ab Merge pull request 'Fixes #936: Sign GET request' (#957) from sign-get into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/957
2021-12-05 22:13:24 +00:00
Kitaiti Makoto 43656d8e46 Create instance for test 2021-12-06 00:33:48 +09:00
Kitaiti Makoto b7cc2369a7 Allow clippy::needless_borrow on CI 2021-12-06 00:33:20 +09:00
Kitaiti Makoto 05a98175bf Assign unsed return value to underscore variable 2021-12-05 21:56:00 +09:00
Kitaiti Makoto 346a67fe1c Create local instance user on start 2021-12-05 21:34:36 +09:00
Kitaiti Makoto a22f5f2336 Add more changelogs 2021-12-05 21:20:37 +09:00
Kitaiti Makoto 55439990c6 Complete changelog 2021-12-05 21:12:50 +09:00
Kitaiti Makoto 3b0f28d061 Update po files 2021-12-05 21:12:14 +09:00
Kitaiti Makoto ac3acfb4ac Merge latest translations 2021-12-05 21:12:14 +09:00
Kitaiti Makoto d5256e9ffc Merge translations from Crowdin 2021-12-05 21:12:14 +09:00
Kitaiti Makoto 13be46445c Add changelog about signing GET requests 2021-12-05 21:10:22 +09:00
Kitaiti Makoto c67b702425 Cache LOCAL_INSTANCE_USER once 2021-12-05 20:57:10 +09:00
Kitaiti Makoto e01539ef16 Follow Clippy 2021-12-05 20:24:11 +09:00
Kitaiti Makoto de4380fd34 Cache local when creating local instance 2021-12-05 20:22:32 +09:00
Kitaiti Makoto 5651e11ba1 Add FIXME comment 2021-12-05 20:22:10 +09:00
KitaitiMakoto 5815602309 Merge pull request 'Don't shrink edit link' (#964) from edit-link-no-shrink into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/964
2021-12-05 10:38:49 +00:00
Kitaiti Makoto a0a69dfb22 Don't shrink edit link 2021-12-05 19:37:38 +09:00
Kitaiti Makoto d98132db80 Don't log unnecessary error 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 25fe2ad802 Use request::get() instead of ClientBuilder 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 48fab8ad2c Impl From for request::Error 2021-12-05 19:32:27 +09:00
Kitaiti Makoto a7d8d49faf Define request::get() function 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 388acd6738 Remove needless reference sign 2021-12-05 19:32:27 +09:00
Kitaiti Makoto d3c035aa39 Remove needless code 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 12a8d00f8e Cache local instance user for test 2021-12-05 19:32:27 +09:00
Kitaiti Makoto b9ea06a01a Don't stop even when caching local instance user 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 1e67b3c13c Create local instance user on caching if it doesn't exist 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 5a5c8bdac8 Make inbox test follow API change 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 44f9d36df1 Make request test follow API change 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 7d349c2de6 Install once_cell 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 79715ec7c7 Add once_cell to dev-dependencies for plume-common 2021-12-05 19:32:27 +09:00
Kitaiti Makoto f4d7dfb261 Sign GET request to other instances 2021-12-05 19:32:27 +09:00
Kitaiti Makoto c525410062 Create local instance user on creating instance 2021-12-05 19:32:27 +09:00
Kitaiti Makoto af5b0b961b Extract Instance::create_local_instance_user() from get_local_instance_user_uncached() 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 2f7a5cbf56 Cache local instance user on start 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 1506802c20 Implement LOCAL_INSTANCE_USER and related methods 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 897ea8e11e Change const name: LOCAL_INSTANCE_USER -> LOCAL_INSTANCE_USERNAME 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 858806149a Use concrete Error for Signer 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 0da9572627 Implement User::get_local_instance_user() 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 94cc260803 Add Instance to users::Role 2021-12-05 19:32:27 +09:00
KitaitiMakoto 7198b06a33 Merge pull request 'Fix problem of PL #956' (#963) from mskf1383/Plume:main into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/963
Reviewed-by: KitaitiMakoto <kitaitimakoto@noreply@joinplu.me>
2021-12-05 10:12:48 +00:00
MohammadSaleh Kamyab 81006e1db8 Update 'assets/themes/default/_global.scss' 2021-12-02 13:16:14 +00:00
KitaitiMakoto 0f7094a70e Merge pull request 'Update crates' (#961) from update-crates into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/961
2021-11-28 00:54:35 +00:00
Kitaiti Makoto a0b661fffe Make template follow change of Rust 2021-11-28 08:48:59 +09:00
Kitaiti Makoto 4bef91f08b Follow clippy warnings 2021-11-28 08:36:27 +09:00
Kitaiti Makoto 20c17be124 Update Rust version 2021-11-28 08:13:16 +09:00
Kitaiti Makoto ae3344f318 Follow Rust and crates update 2021-11-28 08:06:29 +09:00
Kitaiti Makoto 9187e4dde9 Update crates 2021-11-28 08:06:29 +09:00
Kitaiti Makoto 89c185f819 Specify Rocket and serde_json version loosely 2021-11-28 08:06:29 +09:00
Kitaiti Makoto 09b9a37720 Update Rust version 2021-11-28 08:06:24 +09:00
KitaitiMakoto 12a8bfcf2d Merge pull request 'Fix article title overflowing on small screens' (#960) from mareklach/Plume:fix-article-title-overflow-spill into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/960
2021-11-26 19:53:40 +00:00
mareklach b2be00b125 Fix article title overflowing on small screens
Improves CSS scaling of the article title for mobile-sized screens, to prevent it spilling over.
2021-11-26 16:50:08 +00:00
KitaitiMakoto 914d394bd0 Merge pull request 'Stick serde_json versoin for plume-models' (#959) from stick-serde_json into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/959
2021-11-25 23:55:37 +00:00
Kitaiti Makoto 811db1be0e Stick serde_json versoin for plume-models 2021-11-26 08:53:55 +09:00
KitaitiMakoto cd81f042b9 Merge pull request 'Stick serde_json to < 1.0.70' (#958) from stick-serde_json into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/958
2021-11-25 23:35:31 +00:00
Kitaiti Makoto c6111fcd28 Stick serde_json to < 1.0.70 2021-11-26 08:34:38 +09:00
KitaitiMakoto 82ebdc023c Merge pull request 'Fix RTL problem in post card' (#956) from mskf1383/Plume:main into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/956
Reviewed-by: KitaitiMakoto <kitaitimakoto@noreply@joinplu.me>
2021-11-21 14:32:14 +00:00
MohammadSaleh Kamyab 7b110179a9 Update 'assets/themes/default/_global.scss' 2021-11-21 12:21:47 +00:00
MohammadSaleh Kamyab afd66ce7cb Update 'assets/themes/default/_global.scss' 2021-11-21 05:46:48 +00:00
MohammadSaleh Kamyab 9dac97045b Update 'templates/partials/post_card.rs.html' 2021-11-21 05:37:57 +00:00
KitaitiMakoto 188d4ac063 Merge pull request 'Post ap_url as valid IRI' (#947) from post-slug into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/947
2021-09-07 05:36:00 +00:00
Kitaiti Makoto 0fbefe2cdc Percent-encode Post slug on sending Activity to other instances 2021-09-07 13:31:06 +09:00
Kitaiti Makoto eedd5fe4e9 Define utility function to percent-encode for IRI 2021-09-06 17:49:10 +09:00
KitaitiMakoto 27a1a56223 Merge pull request 'Make Feature list more clear' (#946) from github-856 into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/946
2021-09-05 12:50:49 +00:00
Kitaiti Makoto 8e3322776a Merge remote-tracking branch 'origin/main' into github-856 2021-09-05 21:49:50 +09:00
KitaitiMakoto 3d4336c548 Merge pull request 'update some dependancies' (#945) from update-deps into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/945
Reviewed-by: trinity-1686a <trinity-1686a@noreply@joinplu.me>
2021-09-05 12:48:07 +00:00
Trinity Pointard 0c8c607aa4 update some dependancies 2021-09-05 14:41:36 +02:00
D5k H3h 60d6734fe2 Make Feature list more clear
Say it, when something is currently missing.
Otherwise, people will get confused.
2021-09-05 11:58:02 +02:00
KitaitiMakoto 8c372aa6fc Merge pull request 'Fixes #929 Don't stip shipped binaries' (#942) from dont-strip into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/942
2021-08-15 15:46:02 +00:00
Kitaiti Makoto d1a74ca8e6 Don't stip shipped binaries 2021-08-16 00:44:15 +09:00
KitaitiMakoto c374e0af4c Merge pull request 'Upgrade bcrypt create' (#941) from update-blowfish into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/941
2021-08-14 10:23:41 +00:00
Kitaiti Makoto 58e8569048 Install bcrypt 2021-08-14 19:20:30 +09:00
Kitaiti Makoto 83dbf2a945 Upgrade bcrypt 2021-08-14 19:20:21 +09:00
trinity-1686a d7d6d5f644 Merge pull request 'Draw side line for blockquote on start' (#933) from ahangarha/Plume:ahangarha-patch-1-blockquote-line into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/933
Reviewed-by: trinity-1686a <trinity-1686a@noreply@joinplu.me>
2021-07-13 10:16:26 +00:00
trinity-1686a 9aa9885a89 Merge pull request 'Specify rocket version' (#937) from zynnnn/Plume:fix-rocket-version into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/937
Reviewed-by: trinity-1686a <trinity-1686a@noreply@joinplu.me>
2021-07-13 10:15:53 +00:00
Trinity Pointard 7fe1e083e0 use newer pulldown-cmark 2021-07-10 13:34:13 +02:00
Tdxdxoz a946823554 Specify rocket version 2021-07-06 22:36:59 +08:00
ahangarha 3a1872c03e Draw side line for blockquote on start
By this change, the line beside blockquote would appear at start. It means if the text is in LTR language, it appears on left and if it is in RTL, the line appears on right.
2021-06-11 11:53:28 +00:00
KitaitiMakoto 5424f9110f Merge pull request 'Add changelog about change of slug' (#921) from changelog into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/921
2021-04-11 02:45:56 +00:00
Kitaiti Makoto a597816617 Add changelog about change of slug 2021-04-11 11:44:52 +09:00
KitaitiMakoto fa7a44f6bb Merge pull request 'Fix #326 and #721: Keep title in URI' (#920) from keep-title-in-uri into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/920
2021-04-10 08:27:31 +00:00
Kitaiti Makoto dfcdcc1833 Remove heck from plume and plume-models 2021-04-10 16:39:23 +09:00
Kitaiti Makoto 6345a57498 Don't modify article title for slug 2021-04-10 16:37:26 +09:00
Kitaiti Makoto 87457c0ed1 Define Post::slug() 2021-04-10 16:30:50 +09:00
KitaitiMakoto 859a1fd528 Merge pull request 'Set default RUST_LOG to info' (#919) from default-log-level into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/919
2021-04-10 07:04:53 +00:00
Kitaiti Makoto 5069aab584 Set default RUST_LOG to info 2021-04-10 15:59:15 +09:00
KitaitiMakoto 033fde38fe Merge pull request 'Extract Post::ap_url()' (#918) from ap_url into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/918
2021-04-09 08:41:04 +00:00
Kitaiti Makoto 7b2bab0f9d Extract Post::ap_url() 2021-04-09 10:55:09 +09:00
KitaitiMakoto a419ef5319 Merge pull request 'Prevent duplicated posts in 'all' timeline' (#917) from dup-timeline into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/917
2021-03-29 15:31:52 +00:00
Kitaiti Makoto f77dce9f12 Add changelog about fix to duplicated posts in all timeline 2021-03-29 23:35:31 +09:00
Kitaiti Makoto 4c2cd92f0d Prevent duplicated posts in 'all' timeline 2021-03-29 23:33:57 +09:00
KitaitiMakoto dd932e1f15 Merge pull request 'Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured' (#916) from media-directory into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/916
2021-03-28 18:18:14 +00:00
Kitaiti Makoto 4376810d96 Add changelog about fix of MEDIA_UPLOAD_DIRECTORY handling 2021-03-29 02:32:02 +09:00
Kitaiti Makoto c961f4751b Complete pull request number to changelog 2021-03-29 02:29:50 +09:00
Kitaiti Makoto 9e5ec0c9df Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured 2021-03-29 02:29:15 +09:00
KitaitiMakoto 112c034e27 Merge pull request 'Update Post.ap_url when published' (#915) from update-ap-url into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/915
2021-03-28 17:27:45 +00:00
Kitaiti Makoto 664a3ddeea Add changelog about update of ap_url 2021-03-29 02:26:30 +09:00
Kitaiti Makoto 2ffd357d95 Update Post.ap_url when published 2021-03-29 01:38:28 +09:00
KitaitiMakoto 2bc4a13964 Merge pull request 'Add migration to add index to medias.file_path for SQLite3' (#914) from upsert-sqlite into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/914
2021-03-28 14:22:10 +00:00
172 changed files with 10970 additions and 9882 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ commands:
type: boolean
default: false
steps:
- run: cargo clippy <<^parameters.no_feature>>--no-default-features --features="${FEATURES}"<</parameters.no_feature>> --release -p <<parameters.package>> -- -D warnings
- run: cargo clippy <<^parameters.no_feature>>--no-default-features --features="${FEATURES}"<</parameters.no_feature>> --release -p <<parameters.package>> -- -D warnings -A clippy::needless_borrow
run_with_coverage:
description: run command with environment for coverage
+1 -1
View File
@@ -11,7 +11,7 @@ RUN apt update &&\
rm -rf /var/lib/apt/lists/*
#install and configure rust
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2021-01-15 -y &&\
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2021-11-27 -y &&\
rustup component add rustfmt clippy &&\
rustup component add rust-std --target wasm32-unknown-unknown
+1 -1
View File
@@ -16,7 +16,7 @@ DATABASE_URL=postgres://plume:plume@localhost/plume
BASE_URL=plu.me
# Log level for each crate
RUST_LOG=warn,html5ever=warn,hyper=warn,tantivy=warn
RUST_LOG=info
# The secret key for private cookies and CSRF protection
# You can generate one with `openssl rand -base64 32`
@@ -0,0 +1,30 @@
name: cd
on:
push:
branches:
- 'main'
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: plumeorg/plume:latest
+36
View File
@@ -0,0 +1,36 @@
name: cd
on:
push:
tags:
- '*.*.*'
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: plumeorg/plume
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
+34 -2
View File
@@ -4,6 +4,26 @@
## [Unreleased] - ReleaseDate
## [[0.7.1]] - 2022-01-12
### Added
- Introduce environment variable `MAIL_PORT` (#980)
- Introduce email sign-up feature (#636, #1002)
### Changed
- Some styling improvements (#976, #977, #978)
- Respond with error status code when error (#1002)
### Fiexed
- Fix comment link (#974)
- Fix a bug that prevents posting articles (#975)
- Fix a bug that notification page doesn't show (#981)
## [[0.7.0]] - 2022-01-02
### Added
- Allow `dir` attributes for LtoR text in RtoL document (#860)
@@ -11,14 +31,18 @@
- Proxy support (#829)
- Riker a actor system library (#870)
- (request-target) and Host header in HTTP Signature (#872)
- Default log levels for RUST_LOG (#885, #886, #919)
### Changed
- Upgrade some dependent crates (#858)
- Use tracing crate (#868)
- Update Rust version to nightly-2021-01-15 (#878)
- Update Rust version to nightly-2021-11-27 (#961)
- Upgrade Tantivy to 0.13.3 and lindera-tantivy to 0.7.1 (#878)
- Run searcher on actor system (#870)
- Extract a function to calculate posts' ap_url and share it with some places (#918)
- Use article title as its slug instead of capitalizing and inserting hyphens (#920)
- Sign GET requests to other instances (#957)
### Fixed
@@ -26,6 +50,12 @@
- Menu animation not opening on iOS (#876, #897)
- Make actors subscribe to channel once (#913)
- Upsert posts and media instead of trying to insert and fail (#912)
- Update post's ActivityPub id when published by update (#915)
- Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured (#916)
- Prevent duplicated posts in 'all' timeline (#917)
- Draw side line for blockquote on start (#933)
- Fix URIs of posts on Mastodon (#947)
- Place edit link proper position (#956, #963, #964)
## [[0.6.0]] - 2020-12-29
@@ -206,7 +236,9 @@
- Ability to create multiple blogs
<!-- next-url -->
[Unreleased]: https://github.com/Plume-org/Plume/compare/0.6.0...HEAD
[Unreleased]: https://github.com/Plume-org/Plume/compare/0.7.1...HEAD
[[0.7.1]]: https://github.com/Plume-org/Plume/compare/0.7.0...0.7.1
[[0.7.0]]: https://github.com/Plume-org/Plume/compare/0.6.0...0.7.0
[[0.6.0]]: https://github.com/Plume-org/Plume/compare/0.5.0...0.6.0
[0.5.0]: https://github.com/Plume-org/Plume/compare/0.4.0-alpha-4...0.5.0
[0.4.0]: https://github.com/Plume-org/Plume/compare/0.3.0-alpha-2...0.4.0-alpha-4
Generated
+1238 -1010
View File
File diff suppressed because it is too large Load Diff
+8 -13
View File
@@ -1,37 +1,32 @@
[package]
authors = ["Plume contributors"]
name = "plume"
version = "0.6.1-dev"
version = "0.7.1"
repository = "https://github.com/Plume-org/Plume"
edition = "2018"
[dependencies]
activitypub = "0.1.3"
askama_escape = "0.1"
atom_syndication = "0.6"
clap = "2.33"
dotenv = "0.15.0"
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
guid-create = "0.1"
heck = "0.3.0"
lettre = "0.9.2"
guid-create = "0.2"
lettre_email = "0.9.2"
num_cpus = "1.10"
rocket = "0.4.6"
rocket_contrib = { version = "0.4.5", features = ["json"] }
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
rpassword = "4.0"
scheduled-thread-pool = "0.2.2"
serde = "1.0"
serde_json = "1.0"
shrinkwraprs = "0.2.1"
validator = "0.8"
validator_derive = "0.8"
serde_json = "1.0.70"
shrinkwraprs = "0.3.0"
validator = { version = "0.14", features = ["derive"] }
webfinger = "0.4.1"
tracing = "0.1.22"
tracing-subscriber = "0.2.15"
tracing-subscriber = "0.3.5"
riker = "0.4.2"
[[bin]]
@@ -53,7 +48,7 @@ version = "1.4.5"
[dependencies.multipart]
default-features = false
features = ["server"]
version = "0.16"
version = "0.18"
[dependencies.plume-api]
path = "plume-api"
@@ -70,7 +65,7 @@ rev = "29910f2829e7e590a540da3804336577b48c7b31"
[build-dependencies]
ructe = "0.13.0"
rsass = "0.9"
rsass = "0.23"
[features]
default = ["postgres"]
-1
View File
@@ -40,7 +40,6 @@ WORKDIR /app
COPY --from=builder /app /app
COPY --from=builder /usr/local/cargo/bin/plm /bin/
COPY --from=builder /usr/local/cargo/bin/plume /bin/
COPY --from=builder /usr/local/cargo/bin/remove-dup-images /bin/
CMD ["plume"]
+1 -1
View File
@@ -30,7 +30,7 @@ A lot of features are still missing, but what is already here should be quite st
- **Media management**: you can upload pictures to illustrate your articles, but also audio files if you host a podcast, and manage them all from Plume.
- **Federation**: Plume is part of a network of interconnected websites called the Fediverse. Each of these websites (often called *instances*) have their own
rules and thematics, but they can all communicate with each other.
- **Collaborative writing**: invite other people to your blogs, and write articles together.
- **Collaborative writing**: invite other people to your blogs, and write articles together. (Not implemented yet, but will be in 1.0)
## Get involved
+8 -1
View File
@@ -98,7 +98,7 @@ main article {
}
blockquote {
border-left: 5px solid $gray;
border-inline-start: 5px solid $gray;
margin: 1em auto;
padding: 0em 2em;
}
@@ -516,4 +516,11 @@ input:checked ~ .cw-container > .cw-text {
main .article-meta > *, main .article-meta .comments, main .article-meta > .banner > * {
margin: 0 5%;
}
.bottom-bar {
align-items: center;
& > div:nth-child(2) {
margin: 0;
}
}
}
+22 -15
View File
@@ -219,15 +219,23 @@ p.error {
margin: 20px;
}
.cover-link {
margin: 0;
&:hover {
opacity: 0.9;
}
}
.cover {
min-height: 10em;
background-position: center;
background-size: cover;
margin: 0px;
}
&:hover {
opacity: 0.9;
}
header {
display: flex;
}
h3 {
@@ -236,9 +244,14 @@ p.error {
font-family: $playfair;
font-size: 1.75em;
font-weight: normal;
line-height: 1.75;
line-height: 1.10;
display: inline-block;
position: relative;
a {
display: block;
width: 100%;
height: 100%;
padding-block-start: 0.5em;
transition: color 0.1s ease-in;
color: $text-color;
@@ -247,7 +260,8 @@ p.error {
}
.controls {
float: right;
flex-shrink: 0;
text-align: end;
.button {
margin-top: 0;
@@ -261,7 +275,7 @@ p.error {
font-family: $lora;
font-size: 1em;
line-height: 1.25;
text-align: left;
text-align: initial;
overflow: hidden;
}
}
@@ -465,9 +479,10 @@ figure {
/// Avatars
.avatar {
background-position: center;
background-position: center !important;
background-size: cover;
border-radius: 100%;
flex-shrink: 0;
&.small {
width: 50px;
@@ -561,14 +576,6 @@ figure {
}
}
.bottom-bar {
flex-direction: column;
align-items: center;
& > div {
margin: 0;
}
}
main .article-meta .comments .comment {
header {
flex-direction: column;
+13 -7
View File
@@ -41,9 +41,9 @@ fn main() {
.expect("compile templates");
compile_themes().expect("Theme compilation error");
recursive_copy(&Path::new("assets").join("icons"), &Path::new("static"))
recursive_copy(&Path::new("assets").join("icons"), Path::new("static"))
.expect("Couldn't copy icons");
recursive_copy(&Path::new("assets").join("images"), &Path::new("static"))
recursive_copy(&Path::new("assets").join("images"), Path::new("static"))
.expect("Couldn't copy images");
create_dir_all(&Path::new("static").join("media")).expect("Couldn't init media directory");
@@ -97,12 +97,12 @@ fn compile_theme(path: &Path, out_dir: &Path) -> std::io::Result<()> {
.components()
.skip_while(|c| *c != Component::Normal(OsStr::new("themes")))
.skip(1)
.filter_map(|c| {
.map(|c| {
c.as_os_str()
.to_str()
.unwrap_or_default()
.splitn(2, '.')
.next()
.split_once('.')
.map_or(c.as_os_str().to_str().unwrap_or_default(), |x| x.0)
})
.collect::<Vec<_>>()
.join("-");
@@ -120,8 +120,14 @@ fn compile_theme(path: &Path, out_dir: &Path) -> std::io::Result<()> {
// compile the .scss/.sass file
let mut out = File::create(out.join("theme.css"))?;
out.write_all(
&rsass::compile_scss_file(path, rsass::OutputStyle::Compressed)
.expect("SCSS compilation error"),
&rsass::compile_scss_path(
path,
rsass::output::Format {
style: rsass::output::Style::Compressed,
..rsass::output::Format::default()
},
)
.expect("SCSS compilation error"),
)?;
Ok(())
@@ -0,0 +1 @@
DROP TABLE email_signups;
@@ -0,0 +1,9 @@
CREATE TABLE email_signups (
id SERIAL PRIMARY KEY,
email VARCHAR NOT NULL,
token VARCHAR NOT NULL,
expiration_date TIMESTAMP NOT NULL
);
CREATE INDEX email_signups_token ON email_signups (token);
CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email);
@@ -0,0 +1 @@
DROP TABLE email_signups;
@@ -0,0 +1,9 @@
CREATE TABLE email_signups (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
email VARCHAR NOT NULL,
token VARCHAR NOT NULL,
expiration_date TIMESTAMP NOT NULL
);
CREATE INDEX email_signups_token ON email_signups (token);
CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email);
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "plume-api"
version = "0.6.1-dev"
version = "0.7.1"
authors = ["Plume contributors"]
edition = "2018"
+2 -6
View File
@@ -1,6 +1,6 @@
[package]
name = "plume-cli"
version = "0.6.1-dev"
version = "0.7.1"
authors = ["Plume contributors"]
edition = "2018"
@@ -8,13 +8,9 @@ edition = "2018"
name = "plm"
path = "src/main.rs"
[[bin]]
name = "remove-dup-images"
path = "src/remove-dup-images.rs"
[dependencies]
clap = "2.33"
dotenv = "0.14"
dotenv = "0.15"
rpassword = "5.0.0"
[dependencies.diesel]
+2
View File
@@ -68,4 +68,6 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
},
)
.expect("Couldn't save instance");
Instance::cache_local(conn);
Instance::create_local_instance_user(conn).expect("Couldn't save local instance user");
}
+1 -1
View File
@@ -25,7 +25,7 @@ fn main() {
e => e.map(|_| ()).unwrap(),
}
let conn = Conn::establish(CONFIG.database_url.as_str());
let _ = conn.as_ref().map(|conn| Instance::cache_local(conn));
let _ = conn.as_ref().map(Instance::cache_local);
match matches.subcommand() {
("instance", Some(args)) => {
-149
View File
@@ -1,149 +0,0 @@
use diesel::{
BoolExpressionMethods, Connection, ExpressionMethods, JoinOnDsl, NullableExpressionMethods,
QueryDsl, RunQueryDsl,
};
use plume_models::{
blogs::Blog, instance::Instance, medias::Media, posts::Post, Connection as Conn, CONFIG,
};
use std::collections::hash_map::{DefaultHasher, HashMap};
use std::fs::File;
use std::hash::Hasher;
use std::io::{BufReader, Read};
use std::path::Path;
fn main() {
match dotenv::dotenv() {
Ok(path) => eprintln!("Configuration read from {}", path.display()),
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
e => e.map(|_| ()).unwrap(),
}
let conn = Conn::establish(CONFIG.database_url.as_str()).expect("extablish connection");
Instance::cache_local(&conn);
let covers = get_remote_post_covers(&conn);
let remote_media_hashes = calculate_remote_media_hashes(covers);
eprintln!("remote medias: {:?}", remote_media_hashes);
let orphan_medias = get_orphan_medias(&conn);
eprintln!("{:?} orphan media(s)", orphan_medias.len());
for media in orphan_medias {
match calculate_file_hash(&Path::new(&media.file_path)) {
Some(hash) => {
match remote_media_hashes.get(&hash) {
Some(file_path) => {
eprintln!(
"File already referred. Removes only medias record. {}",
&file_path
);
// Remove medias record
diesel::delete(&media)
.execute(&conn)
.expect("Delete medias record");
}
None => {
eprintln!("Removes {}", &media.file_path);
// Remove file and medias record
media.delete(&conn).expect("Delete media record and file");
}
}
}
None => {
eprintln!(
"File doesn't exist. Removes medias record. medias.id: {}, path: {}",
&media.id, &media.file_path
);
diesel::delete(&media)
.execute(&conn)
.expect("Delete medias record");
}
}
}
}
fn get_remote_post_covers(conn: &Conn) -> Vec<Media> {
use plume_models::schema::blogs;
use plume_models::schema::posts;
let remote_instances = Instance::get_remotes(&conn).expect("get remote instances");
let remote_instance_ids = remote_instances.iter().map(|instance| instance.id);
let remote_blogs = blogs::table
.filter(blogs::instance_id.eq_any(remote_instance_ids))
.load::<Blog>(conn)
.expect("remote blogs");
let remote_blog_ids = remote_blogs.iter().map(|blog| blog.id);
let remote_posts = posts::table
.filter(posts::blog_id.eq_any(remote_blog_ids))
.load::<Post>(conn)
.expect("remote posts");
remote_posts
.iter()
.filter_map(|post| post.cover_id)
.map(|cover_id| Media::get(conn, cover_id).expect("Media"))
.collect()
}
fn calculate_remote_media_hashes(medias: Vec<Media>) -> HashMap<u64, String> {
let mut media_hashes = HashMap::new();
for media in medias.iter() {
if let Some(hash) = calculate_file_hash(Path::new(&media.file_path)) {
let _ = media_hashes.insert(hash, media.file_path.clone());
}
}
media_hashes
}
fn calculate_file_hash(path: &Path) -> Option<u64> {
if !path.exists() {
return None;
}
let file = File::open(path).expect("open file");
let mut reader = BufReader::new(file);
let mut hasher = DefaultHasher::new();
let mut buffer = [0; 2048];
while let Ok(n) = reader.read(&mut buffer) {
hasher.write(&buffer);
if n == 0 {
break;
}
}
Some(hasher.finish())
}
fn get_orphan_medias(conn: &Conn) -> Vec<Media> {
use plume_models::schema::{self, medias};
use plume_models::schema::{blogs::dsl::blogs, posts::dsl::posts, users::dsl::users};
let query = medias::table
.select((
medias::id,
medias::file_path,
medias::alt_text,
medias::is_remote,
medias::remote_url,
medias::sensitive,
medias::content_warning,
medias::owner_id,
))
.left_outer_join(users.on(schema::users::avatar_id.eq(medias::id.nullable())))
.left_outer_join(
blogs.on(schema::blogs::icon_id
.eq(medias::id.nullable())
.or(schema::blogs::banner_id.eq(medias::id.nullable()))),
)
.left_outer_join(posts.on(schema::posts::cover_id.eq(medias::id.nullable())))
.filter(
schema::users::avatar_id.is_null().and(
schema::blogs::icon_id.is_null().and(
schema::blogs::banner_id.is_null().and(
schema::posts::cover_id
.is_null()
.and(medias::is_remote.eq(false)),
),
),
),
);
eprintln!(
"query for orphan medias: {}",
diesel::debug_query::<_, _>(&query)
);
query.load::<Media>(conn).expect("Load orphan medias")
}
+11 -6
View File
@@ -1,6 +1,6 @@
[package]
name = "plume-common"
version = "0.6.1-dev"
version = "0.7.1"
authors = ["Plume contributors"]
edition = "2018"
@@ -9,21 +9,21 @@ activitypub = "0.1.1"
activitystreams-derive = "0.1.1"
activitystreams-traits = "0.1.0"
array_tool = "1.0"
base64 = "0.10"
heck = "0.3.0"
hex = "0.3"
hyper = "0.12.33"
base64 = "0.13"
heck = "0.4.0"
hex = "0.4"
openssl = "0.10.22"
rocket = "0.4.6"
reqwest = { version = "0.9", features = ["socks"] }
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
serde_json = "1.0.70"
shrinkwraprs = "0.3.0"
syntect = "4.5.0"
tokio = "0.1.22"
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
tracing = "0.1.22"
askama_escape = "0.10.2"
[dependencies.chrono]
features = ["serde"]
@@ -33,3 +33,8 @@ version = "0.4"
default-features = false
git = "https://git.joinplu.me/Plume/pulldown-cmark"
branch = "bidi-plume"
[dev-dependencies]
once_cell = "1.5.2"
[features]
+174 -31
View File
@@ -1,6 +1,8 @@
use reqwest::header::{HeaderValue, ACCEPT};
use reqwest;
use std::fmt::Debug;
use super::{request, sign::Signer};
/// Represents an ActivityPub inbox.
///
/// It routes an incoming Activity through the registered handlers.
@@ -10,7 +12,50 @@ use std::fmt::Debug;
/// ```rust
/// # extern crate activitypub;
/// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note};
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
/// # use once_cell::sync::Lazy;
/// # use plume_common::activity_pub::inbox::*;
/// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer};
/// #
/// # static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
/// #
/// # struct MySigner {
/// # public_key: String,
/// # private_key: String,
/// # }
/// #
/// # impl MySigner {
/// # fn new() -> Self {
/// # let (pub_key, priv_key) = gen_keypair();
/// # Self {
/// # public_key: String::from_utf8(pub_key).unwrap(),
/// # private_key: String::from_utf8(priv_key).unwrap(),
/// # }
/// # }
/// # }
/// #
/// # impl Signer for MySigner {
/// # fn get_key_id(&self) -> String {
/// # "mysigner".into()
/// # }
/// #
/// # fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
/// # let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
/// # .unwrap();
/// # let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
/// # signer.update(to_sign.as_bytes()).unwrap();
/// # signer.sign_to_vec().map_err(|_| SignError())
/// # }
/// #
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
/// # let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
/// # .unwrap();
/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
/// # verifier.update(data.as_bytes()).unwrap();
/// # verifier.verify(&signature).map_err(|_| SignError())
/// # }
/// # }
/// #
/// # struct User;
/// # impl FromId<()> for User {
/// # type Error = ();
@@ -23,6 +68,10 @@ use std::fmt::Debug;
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
/// # Ok(User)
/// # }
/// #
/// # fn get_sender() -> &'static dyn Signer {
/// # &*MY_SIGNER
/// # }
/// # }
/// # impl AsActor<&()> for User {
/// # fn get_inbox_url(&self) -> String {
@@ -42,6 +91,10 @@ use std::fmt::Debug;
/// # fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
/// # Ok(Message)
/// # }
/// #
/// # fn get_sender() -> &'static dyn Signer {
/// # &*MY_SIGNER
/// # }
/// # }
/// # impl AsObject<User, Create, &()> for Message {
/// # type Error = ();
@@ -207,7 +260,7 @@ where
};
// Handle the activity
match obj.activity(ctx, actor, &act_id) {
match obj.activity(ctx, actor, act_id) {
Ok(res) => Inbox::Handled(res.into()),
Err(e) => Inbox::Failed(e),
}
@@ -311,35 +364,16 @@ pub trait FromId<C>: Sized {
id: &str,
proxy: Option<reqwest::Proxy>,
) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
if let Some(proxy) = proxy {
reqwest::ClientBuilder::new().proxy(proxy)
} else {
reqwest::ClientBuilder::new()
}
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()
.map_err(|_| (None, InboxError::DerefError.into()))?
.get(id)
.header(
ACCEPT,
HeaderValue::from_str(
&super::ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)
.map_err(|_| (None, InboxError::DerefError.into()))?,
)
.send()
.map_err(|_| (None, InboxError::DerefError))
.and_then(|mut r| {
let json: serde_json::Value = r
.json()
.map_err(|_| (None, InboxError::InvalidObject(None)))?;
serde_json::from_value(json.clone())
.map_err(|_| (Some(json), InboxError::InvalidObject(None)))
})
.map_err(|(json, e)| (json, e.into()))
request::get(id, Self::get_sender(), proxy)
.map_err(|_| (None, InboxError::DerefError))
.and_then(|mut r| {
let json: serde_json::Value = r
.json()
.map_err(|_| (None, InboxError::InvalidObject(None)))?;
serde_json::from_value(json.clone())
.map_err(|_| (Some(json), InboxError::InvalidObject(None)))
})
.map_err(|(json, e)| (json, e.into()))
}
/// Builds a `Self` from its ActivityPub representation
@@ -347,6 +381,8 @@ pub trait FromId<C>: Sized {
/// Tries to find a `Self` with a given ID (`id`), using `ctx` (a database)
fn from_db(ctx: &C, id: &str) -> Result<Self, Self::Error>;
fn get_sender() -> &'static dyn Signer;
}
/// Should be implemented by anything representing an ActivityPub actor.
@@ -385,6 +421,49 @@ pub trait AsActor<C> {
/// # extern crate activitypub;
/// # use activitypub::{activity::Create, actor::Person, object::Note};
/// # use plume_common::activity_pub::inbox::{AsActor, AsObject, FromId};
/// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer};
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
/// # use once_cell::sync::Lazy;
/// #
/// # static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
/// #
/// # struct MySigner {
/// # public_key: String,
/// # private_key: String,
/// # }
/// #
/// # impl MySigner {
/// # fn new() -> Self {
/// # let (pub_key, priv_key) = gen_keypair();
/// # Self {
/// # public_key: String::from_utf8(pub_key).unwrap(),
/// # private_key: String::from_utf8(priv_key).unwrap(),
/// # }
/// # }
/// # }
/// #
/// # impl Signer for MySigner {
/// # fn get_key_id(&self) -> String {
/// # "mysigner".into()
/// # }
/// #
/// # fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
/// # let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
/// # .unwrap();
/// # let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
/// # signer.update(to_sign.as_bytes()).unwrap();
/// # signer.sign_to_vec().map_err(|_| SignError())
/// # }
/// #
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
/// # let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
/// # .unwrap();
/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
/// # verifier.update(data.as_bytes()).unwrap();
/// # verifier.verify(&signature).map_err(|_| SignError())
/// # }
/// # }
/// #
/// # struct Account;
/// # impl FromId<()> for Account {
/// # type Error = ();
@@ -397,6 +476,10 @@ pub trait AsActor<C> {
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
/// # Ok(Account)
/// # }
/// #
/// # fn get_sender() -> &'static dyn Signer {
/// # &*MY_SIGNER
/// # }
/// # }
/// # impl AsActor<()> for Account {
/// # fn get_inbox_url(&self) -> String {
@@ -420,6 +503,10 @@ pub trait AsActor<C> {
/// fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
/// Ok(Message { text: obj.object_props.content_string().map_err(|_| ())? })
/// }
///
/// fn get_sender() -> &'static dyn Signer {
/// &*MY_SIGNER
/// }
/// }
///
/// impl AsObject<Account, Create, ()> for Message {
@@ -459,7 +546,51 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::activity_pub::sign::{
gen_keypair, Error as SignError, Result as SignResult, Signer,
};
use activitypub::{activity::*, actor::Person, object::Note};
use once_cell::sync::Lazy;
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
struct MySigner {
public_key: String,
private_key: String,
}
impl MySigner {
fn new() -> Self {
let (pub_key, priv_key) = gen_keypair();
Self {
public_key: String::from_utf8(pub_key).unwrap(),
private_key: String::from_utf8(priv_key).unwrap(),
}
}
}
impl Signer for MySigner {
fn get_key_id(&self) -> String {
"mysigner".into()
}
fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
.unwrap();
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
signer.update(to_sign.as_bytes()).unwrap();
signer.sign_to_vec().map_err(|_| SignError())
}
fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
.unwrap();
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
verifier.update(data.as_bytes()).unwrap();
verifier.verify(&signature).map_err(|_| SignError())
}
}
struct MyActor;
impl FromId<()> for MyActor {
@@ -473,6 +604,10 @@ mod tests {
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
Ok(MyActor)
}
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
}
}
impl AsActor<&()> for MyActor {
@@ -497,6 +632,10 @@ mod tests {
fn from_activity(_: &(), _obj: Note) -> Result<Self, Self::Error> {
Ok(MyObject)
}
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
}
}
impl AsObject<MyActor, Create, &()> for MyObject {
type Error = ();
@@ -601,6 +740,10 @@ mod tests {
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
Err(())
}
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
}
}
impl AsActor<&()> for FailingActor {
fn get_inbox_url(&self) -> String {
+1 -1
View File
@@ -145,7 +145,7 @@ where
warn!("Inbox doesn't have host: {:?}", &inbox);
continue;
};
let host_header_value = HeaderValue::from_str(&url.host_str().expect("Unreachable"));
let host_header_value = HeaderValue::from_str(url.host_str().expect("Unreachable"));
if host_header_value.is_err() {
warn!("Header value is invalid: {:?}", url.host_str());
continue;
+55 -9
View File
@@ -1,6 +1,11 @@
use chrono::{offset::Utc, DateTime};
use openssl::hash::{Hasher, MessageDigest};
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT};
use reqwest::{
header::{
HeaderMap, HeaderValue, InvalidHeaderValue, ACCEPT, CONTENT_TYPE, DATE, HOST, USER_AGENT,
},
ClientBuilder, Proxy, Response, Url, UrlError,
};
use std::ops::Deref;
use std::time::SystemTime;
use tracing::warn;
@@ -13,6 +18,24 @@ const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
#[derive(Debug)]
pub struct Error();
impl From<UrlError> for Error {
fn from(_err: UrlError) -> Self {
Error()
}
}
impl From<InvalidHeaderValue> for Error {
fn from(_err: InvalidHeaderValue) -> Self {
Error()
}
}
impl From<reqwest::Error> for Error {
fn from(_err: reqwest::Error) -> Self {
Error()
}
}
pub struct Digest(String);
impl Digest {
@@ -118,8 +141,8 @@ type Path<'a> = &'a str;
type Query<'a> = &'a str;
type RequestTarget<'a> = (Method<'a>, Path<'a>, Option<Query<'a>>);
pub fn signature<S: Signer>(
signer: &S,
pub fn signature(
signer: &dyn Signer,
headers: &HeaderMap,
request_target: RequestTarget,
) -> Result<HeaderValue, Error> {
@@ -164,10 +187,35 @@ pub fn signature<S: Signer>(
)).map_err(|_| Error())
}
pub fn get(url_str: &str, sender: &dyn Signer, proxy: Option<Proxy>) -> Result<Response, Error> {
let mut headers = headers();
let url = Url::parse(url_str)?;
if !url.has_host() {
return Err(Error());
}
let host_header_value = HeaderValue::from_str(url.host_str().expect("Unreachable"))?;
headers.insert(HOST, host_header_value);
if let Some(proxy) = proxy {
ClientBuilder::new().proxy(proxy)
} else {
ClientBuilder::new()
}
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(url_str)
.headers(headers.clone())
.header(
"Signature",
signature(sender, &headers, ("get", url.path(), url.query()))?,
)
.send()
.map_err(|_| Error())
}
#[cfg(test)]
mod tests {
use super::{signature, Error};
use crate::activity_pub::sign::{gen_keypair, Signer};
use super::signature;
use crate::activity_pub::sign::{gen_keypair, Error, Result, Signer};
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
use reqwest::header::HeaderMap;
@@ -187,13 +235,11 @@ mod tests {
}
impl Signer for MySigner {
type Error = Error;
fn get_key_id(&self) -> String {
"mysigner".into()
}
fn sign(&self, to_sign: &str) -> Result<Vec<u8>, Self::Error> {
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
.unwrap();
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
@@ -201,7 +247,7 @@ mod tests {
signer.sign_to_vec().map_err(|_| Error())
}
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error> {
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
.unwrap();
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
+12 -7
View File
@@ -19,20 +19,25 @@ pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
#[derive(Debug)]
pub struct Error();
pub type Result<T> = std::result::Result<T, Error>;
impl From<openssl::error::ErrorStack> for Error {
fn from(_: openssl::error::ErrorStack) -> Self {
Self()
}
}
pub trait Signer {
type Error;
fn get_key_id(&self) -> String;
/// Sign some data with the signer keypair
fn sign(&self, to_sign: &str) -> Result<Vec<u8>, Self::Error>;
fn sign(&self, to_sign: &str) -> Result<Vec<u8>>;
/// Verify if the signature is valid
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error>;
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool>;
}
pub trait Signable {
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self, Error>
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self>
where
T: Signer;
fn verify<T>(self, creator: &T) -> bool
@@ -46,7 +51,7 @@ pub trait Signable {
}
impl Signable for serde_json::Value {
fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value, Error> {
fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value> {
let creation_date = Utc::now().to_rfc3339();
let mut options = json!({
"type": "RsaSignature2017",
@@ -182,7 +187,7 @@ pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
}
let digest = all_headers.get_one("digest").unwrap_or("");
let digest = request::Digest::from_header(digest);
if !digest.map(|d| d.verify_header(&data)).unwrap_or(false) {
if !digest.map(|d| d.verify_header(data)).unwrap_or(false) {
// signature was valid, but body content does not match its digest
return SignatureValidity::Invalid;
}
+76 -22
View File
@@ -1,11 +1,8 @@
use heck::CamelCase;
use heck::ToUpperCamelCase;
use openssl::rand::rand_bytes;
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag};
use regex_syntax::is_word_character;
use rocket::{
http::uri::Uri,
response::{Flash, Redirect},
};
use rocket::http::uri::Uri;
use std::collections::HashSet;
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
use syntect::parsing::SyntaxSet;
@@ -21,23 +18,63 @@ pub fn random_hex() -> String {
/// Remove non alphanumeric characters and CamelCase a string
pub fn make_actor_id(name: &str) -> String {
name.to_camel_case()
name.to_upper_camel_case()
.chars()
.filter(|c| c.is_alphanumeric())
.collect()
}
/**
* Redirects to the login page with a given message.
*
* Note that the message should be translated before passed to this function.
*/
pub fn requires_login<T: Into<Uri<'static>>>(message: &str, url: T) -> Flash<Redirect> {
Flash::new(
Redirect::to(format!("/login?m={}", Uri::percent_encode(message))),
"callback",
url.into().to_string(),
)
* Percent-encode characters which are not allowed in IRI path segments.
*
* Intended to be used for generating Post ap_url.
*/
pub fn iri_percent_encode_seg(segment: &str) -> String {
segment.chars().map(iri_percent_encode_seg_char).collect()
}
pub fn iri_percent_encode_seg_char(c: char) -> String {
if c.is_alphanumeric() {
c.to_string()
} else {
match c {
'-'
| '.'
| '_'
| '~'
| '\u{A0}'..='\u{D7FF}'
| '\u{20000}'..='\u{2FFFD}'
| '\u{30000}'..='\u{3FFFD}'
| '\u{40000}'..='\u{4FFFD}'
| '\u{50000}'..='\u{5FFFD}'
| '\u{60000}'..='\u{6FFFD}'
| '\u{70000}'..='\u{7FFFD}'
| '\u{80000}'..='\u{8FFFD}'
| '\u{90000}'..='\u{9FFFD}'
| '\u{A0000}'..='\u{AFFFD}'
| '\u{B0000}'..='\u{BFFFD}'
| '\u{C0000}'..='\u{CFFFD}'
| '\u{D0000}'..='\u{DFFFD}'
| '\u{E0000}'..='\u{EFFFD}'
| '!'
| '$'
| '&'
| '\''
| '('
| ')'
| '*'
| '+'
| ','
| ';'
| '='
| ':'
| '@' => c.to_string(),
_ => {
let s = c.to_string();
Uri::percent_encode(&s).to_string()
}
}
}
}
#[derive(Debug)]
@@ -88,13 +125,13 @@ fn highlight_code<'a>(
unreachable!();
};
let syntax_set = SyntaxSet::load_defaults_newlines();
let syntax = syntax_set.find_syntax_by_token(&lang).unwrap_or_else(|| {
let syntax = syntax_set.find_syntax_by_token(lang).unwrap_or_else(|| {
syntax_set
.find_syntax_by_name(&lang)
.find_syntax_by_name(lang)
.unwrap_or_else(|| syntax_set.find_syntax_plain_text())
});
let mut html = ClassedHTMLGenerator::new_with_class_style(
&syntax,
syntax,
&syntax_set,
ClassStyle::Spaced,
);
@@ -281,16 +318,15 @@ pub fn md_to_html<'a>(
text_acc.push(c)
}
let mention = text_acc;
let short_mention = mention.splitn(1, '@').next().unwrap_or("");
let link = Tag::Link(
LinkType::Inline,
format!("{}@/{}/", base_url, &mention).into(),
short_mention.to_owned().into(),
mention.clone().into(),
);
mentions.push(mention.clone());
events.push(Event::Start(link.clone()));
events.push(Event::Text(format!("@{}", &short_mention).into()));
events.push(Event::Text(format!("@{}", &mention).into()));
events.push(Event::End(link));
(
@@ -414,6 +450,10 @@ pub fn md_to_html<'a>(
(buf, mentions.collect(), hashtags.collect())
}
pub fn escape(string: &str) -> askama_escape::Escaped<askama_escape::Html> {
askama_escape::escape(string, askama_escape::Html)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -476,6 +516,20 @@ mod tests {
}
}
#[test]
fn test_iri_percent_encode_seg() {
assert_eq!(
&iri_percent_encode_seg("including whitespace"),
"including%20whitespace"
);
assert_eq!(&iri_percent_encode_seg("%20"), "%2520");
assert_eq!(&iri_percent_encode_seg("é"), "é");
assert_eq!(
&iri_percent_encode_seg("空白入り 日本語"),
"空白入り%20日本語"
);
}
#[test]
fn test_inline() {
assert_eq!(
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "plume-front"
version = "0.6.1-dev"
version = "0.7.1"
authors = ["Plume contributors"]
edition = "2018"
+20 -14
View File
@@ -54,11 +54,6 @@ pub enum EditorError {
DOMError,
}
impl From<std::option::NoneError> for EditorError {
fn from(_: std::option::NoneError) -> Self {
EditorError::NoneError
}
}
const AUTOSAVE_DEBOUNCE_TIME: i32 = 5000;
#[derive(Serialize, Deserialize)]
struct AutosaveInformation {
@@ -198,7 +193,7 @@ fn clear_autosave() {
.unwrap()
.remove_item(&get_autosave_id())
.unwrap();
console::log_1(&&format!("Saved to {}", &get_autosave_id()).into());
console::log_1(&format!("Saved to {}", &get_autosave_id()).into());
}
type TimeoutHandle = i32;
lazy_static! {
@@ -366,7 +361,9 @@ fn init_editor() -> Result<(), EditorError> {
return Ok(());
}
let old_ed = old_ed.unwrap();
let old_title = document().get_element_by_id("plume-editor-title")?;
let old_title = document()
.get_element_by_id("plume-editor-title")
.ok_or(EditorError::NoneError)?;
old_ed
.dyn_ref::<HtmlElement>()
.unwrap()
@@ -434,7 +431,8 @@ fn init_editor() -> Result<(), EditorError> {
bg.class_list().add_1("show").unwrap();
})) as Box<dyn FnMut(MouseEvent)>);
document()
.get_element_by_id("publish")?
.get_element_by_id("publish")
.ok_or(EditorError::NoneError)?
.add_event_listener_with_callback("click", show_popup.as_ref().unchecked_ref())
.map_err(|_| EditorError::DOMError)?;
show_popup.forget();
@@ -528,8 +526,14 @@ fn init_popup(
cover_label
.set_attribute("for", "cover")
.map_err(|_| EditorError::DOMError)?;
let cover = document.get_element_by_id("cover")?;
cover.parent_element()?.remove_child(&cover).ok();
let cover = document
.get_element_by_id("cover")
.ok_or(EditorError::NoneError)?;
cover
.parent_element()
.ok_or(EditorError::NoneError)?
.remove_child(&cover)
.ok();
popup
.append_child(&cover_label)
.map_err(|_| EditorError::DOMError)?;
@@ -554,7 +558,7 @@ fn init_popup(
draft.set_checked(draft_checkbox.checked());
draft_label
.append_child(&draft)
.append_child(draft)
.map_err(|_| EditorError::DOMError)?;
draft_label
.append_child(&document.create_text_node(&i18n!(CATALOG, "This is a draft")))
@@ -620,11 +624,12 @@ fn init_popup(
.map_err(|_| EditorError::DOMError)?;
callback.forget();
popup
.append_child(&button)
.append_child(button)
.map_err(|_| EditorError::DOMError)?;
document
.body()?
.body()
.ok_or(EditorError::NoneError)?
.append_child(&popup)
.map_err(|_| EditorError::DOMError)?;
Ok(popup)
@@ -641,7 +646,8 @@ fn init_popup_bg() -> Result<Element, EditorError> {
.map_err(|_| EditorError::DOMError)?;
document()
.body()?
.body()
.ok_or(EditorError::NoneError)?
.append_child(&bg)
.map_err(|_| EditorError::DOMError)?;
let callback = Closure::wrap(Box::new(|_| close_popup()) as Box<dyn FnMut(MouseEvent)>);
+3 -3
View File
@@ -1,5 +1,5 @@
#![recursion_limit = "128"]
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
#![feature(decl_macro, proc_macro_hygiene)]
#[macro_use]
extern crate gettext_macros;
@@ -61,7 +61,7 @@ lazy_static! {
static ref CATALOG: gettext::Catalog = {
let catalogs = include_i18n!();
let lang = window().unwrap().navigator().language().unwrap();
let lang = lang.splitn(2, '-').next().unwrap_or("en");
let lang = lang.split_once('-').map_or("en", |x| x.0);
let english_position = catalogs
.iter()
@@ -85,7 +85,7 @@ pub fn main() -> Result<(), JsValue> {
menu();
search();
editor::init()
.map_err(|e| console::error_1(&&format!("Editor error: {:?}", e).into()))
.map_err(|e| console::error_1(&format!("Editor error: {:?}", e).into()))
.ok();
Ok(())
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "plume-macro"
version = "0.6.1-dev"
version = "0.7.1"
authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"]
edition = "2018"
description = "Plume procedural macros"
+2 -2
View File
@@ -58,7 +58,7 @@ pub fn import_migrations(input: TokenStream) -> TokenStream {
(name, up_sql, down_sql)
})
.collect::<Vec<_>>();
let migrations_name = migrations.iter().map(|m| &m.0).collect::<Vec<_>>();
let migrations_name = migrations.iter().map(|m| &m.0);
let migrations_up = migrations
.iter()
.map(|m| m.1.as_str())
@@ -103,7 +103,7 @@ fn file_to_migration(file: &str) -> TokenStream2 {
acc.push('\n');
}
} else if let Some(acc_str) = line.strip_prefix("--#!") {
acc.push_str(&acc_str);
acc.push_str(acc_str);
acc.push('\n');
} else if line.starts_with("--") {
continue;
+11 -11
View File
@@ -1,19 +1,17 @@
[package]
name = "plume-models"
version = "0.6.1-dev"
version = "0.7.1"
authors = ["Plume contributors"]
edition = "2018"
[dependencies]
activitypub = "0.1.1"
ammonia = "2.1.1"
askama_escape = "0.1"
bcrypt = "0.5"
guid-create = "0.1"
heck = "0.3.0"
itertools = "0.8.0"
ammonia = "3.1.2"
bcrypt = "0.10.1"
guid-create = "0.2"
itertools = "0.10.3"
lazy_static = "1.0"
ldap3 = "0.7.1"
ldap3 = "0.9.3"
migrations_internals= "1.4.0"
openssl = "0.10.22"
rocket = "0.4.6"
@@ -22,19 +20,21 @@ reqwest = "0.9"
scheduled-thread-pool = "0.2.2"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
serde_json = "1.0.70"
tantivy = "0.13.3"
url = "2.1"
walkdir = "2.2"
webfinger = "0.4.1"
whatlang = "0.11.1"
shrinkwraprs = "0.2.1"
whatlang = "0.13.0"
shrinkwraprs = "0.3.0"
diesel-derive-newtype = "0.1.2"
glob = "0.3.0"
lindera-tantivy = { version = "0.7.1", optional = true }
tracing = "0.1.22"
riker = "0.4.2"
once_cell = "1.5.2"
lettre = "0.9.6"
native-tls = "0.2.8"
[dependencies.chrono]
features = ["serde"]
+12 -8
View File
@@ -86,14 +86,18 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
}
let mut parsed_header = headers[0].split(' ');
let auth_type = parsed_header.next().map_or_else(
|| Outcome::Failure((Status::BadRequest, TokenError::NoType)),
Outcome::Success,
)?;
let val = parsed_header.next().map_or_else(
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
Outcome::Success,
)?;
let auth_type = parsed_header
.next()
.map_or_else::<rocket::Outcome<&str, _, ()>, _, _>(
|| Outcome::Failure((Status::BadRequest, TokenError::NoType)),
Outcome::Success,
)?;
let val = parsed_header
.next()
.map_or_else::<rocket::Outcome<&str, _, ()>, _, _>(
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
Outcome::Success,
)?;
if auth_type == "Bearer" {
let conn = request
+1 -1
View File
@@ -28,7 +28,7 @@ impl BlocklistedEmail {
pub fn delete_entries(conn: &Connection, ids: Vec<i32>) -> Result<bool> {
use diesel::delete;
for i in ids {
let be: BlocklistedEmail = BlocklistedEmail::find_by_id(&conn, i)?;
let be: BlocklistedEmail = BlocklistedEmail::find_by_id(conn, i)?;
delete(&be).execute(conn)?;
}
Ok(true)
+26 -13
View File
@@ -149,7 +149,15 @@ impl Blog {
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger)
.and_then(|l| Blog::from_id(conn, &l.href?, None, CONFIG.proxy()).map_err(|(_, e)| e))
.and_then(|l| {
Blog::from_id(
conn,
&l.href.ok_or(Error::MissingApProperty)?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)
})
}
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
@@ -236,7 +244,7 @@ impl Blog {
(min, max): (i32, i32),
) -> Result<ActivityStream<OrderedCollectionPage>> {
let mut coll = OrderedCollectionPage::default();
let acts = self.get_activity_page(&conn, (min, max));
let acts = self.get_activity_page(conn, (min, max));
//This still doesn't do anything because the outbox
//doesn't do anything yet
coll.collection_page_props.set_next_link(Id::new(&format!(
@@ -265,7 +273,10 @@ impl Blog {
pub fn get_keypair(&self) -> Result<PKey<Private>> {
PKey::from_rsa(Rsa::private_key_from_pem(
self.private_key.clone()?.as_ref(),
self.private_key
.clone()
.ok_or(Error::MissingApProperty)?
.as_ref(),
)?)
.map_err(Error::from)
}
@@ -318,7 +329,7 @@ impl Blog {
}
pub fn delete(&self, conn: &Connection) -> Result<()> {
for post in Post::get_for_blog(conn, &self)? {
for post in Post::get_for_blog(conn, self)? {
post.delete(conn)?;
}
diesel::delete(self)
@@ -339,12 +350,12 @@ impl FromId<DbConn> for Blog {
type Object = CustomGroup;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Self::find_by_ap_url(&conn, id)
Self::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result<Self> {
let url = Url::parse(&acct.object.object_props.id_string()?)?;
let inst = url.host_str()?;
let inst = url.host_str().ok_or(Error::Url)?;
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
Instance::insert(
conn,
@@ -432,6 +443,10 @@ impl FromId<DbConn> for Blog {
},
)
}
fn get_sender() -> &'static dyn sign::Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl AsActor<&PlumeRocket> for Blog {
@@ -451,24 +466,22 @@ impl AsActor<&PlumeRocket> for Blog {
}
impl sign::Signer for Blog {
type Error = Error;
fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url)
}
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = self.get_keypair()?;
fn sign(&self, to_sign: &str) -> sign::Result<Vec<u8>> {
let key = self.get_keypair().map_err(|_| sign::Error())?;
let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?;
signer.sign_to_vec().map_err(Error::from)
signer.sign_to_vec().map_err(sign::Error::from)
}
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
fn verify(&self, data: &str, signature: &[u8]) -> sign::Result<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?;
verifier.verify(&signature).map_err(Error::from)
verifier.verify(signature).map_err(sign::Error::from)
}
}
+22 -7
View File
@@ -21,6 +21,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use plume_common::{
activity_pub::{
inbox::{AsActor, AsObject, FromId},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY,
},
utils,
@@ -141,18 +142,20 @@ impl Comment {
}
pub fn create_activity(&self, conn: &DbConn) -> Result<Create> {
let author = User::get(&conn, self.author_id)?;
let author = User::get(conn, self.author_id)?;
let note = self.to_activity(conn)?;
let mut act = Create::default();
act.create_props.set_actor_link(author.into_id())?;
act.create_props.set_object_object(note.clone())?;
act.object_props
.set_id_string(format!("{}/activity", self.ap_url.clone()?,))?;
act.object_props.set_id_string(format!(
"{}/activity",
self.ap_url.clone().ok_or(Error::MissingApProperty)?,
))?;
act.object_props
.set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?;
act.object_props
.set_cc_link_vec(vec![Id::new(self.get_author(&conn)?.followers_endpoint)])?;
.set_cc_link_vec(vec![Id::new(self.get_author(conn)?.followers_endpoint)])?;
Ok(act)
}
@@ -182,7 +185,9 @@ impl Comment {
.set_actor_link(self.get_author(conn)?.into_id())?;
let mut tombstone = Tombstone::default();
tombstone.object_props.set_id_string(self.ap_url.clone()?)?;
tombstone
.object_props
.set_id_string(self.ap_url.clone().ok_or(Error::MissingApProperty)?)?;
act.delete_props.set_object_object(tombstone)?;
act.object_props
@@ -204,7 +209,13 @@ impl FromId<DbConn> for Comment {
fn from_activity(conn: &DbConn, note: Note) -> Result<Self> {
let comm = {
let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?;
let previous_url = note
.object_props
.in_reply_to
.as_ref()
.ok_or(Error::MissingApProperty)?
.as_str()
.ok_or(Error::MissingApProperty)?;
let previous_comment = Comment::find_by_ap_url(conn, previous_url);
let is_public = |v: &Option<serde_json::Value>| match v
@@ -318,6 +329,10 @@ impl FromId<DbConn> for Comment {
comm.notify(conn)?;
Ok(comm)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl AsObject<User, Create, &DbConn> for Comment {
@@ -346,7 +361,7 @@ impl AsObject<User, Delete, &DbConn> for Comment {
m.delete(conn)?;
}
for n in Notification::find_for_comment(&conn, &self)? {
for n in Notification::find_for_comment(conn, &self)? {
n.delete(&**conn)?;
}
+42 -11
View File
@@ -1,4 +1,6 @@
use crate::search::TokenizerKind as SearchTokenizer;
use crate::signups::Strategy as SignupStrategy;
use crate::smtp::{SMTP_PORT, SUBMISSIONS_PORT, SUBMISSION_PORT};
use rocket::config::Limits;
use rocket::Config as RocketConfig;
use std::collections::HashSet;
@@ -15,12 +17,14 @@ pub struct Config {
pub db_name: &'static str,
pub db_max_size: Option<u32>,
pub db_min_idle: Option<u32>,
pub signup: SignupStrategy,
pub search_index: String,
pub search_tokenizers: SearchTokenizerConfig,
pub rocket: Result<RocketConfig, RocketError>,
pub rocket: Result<RocketConfig, InvalidRocketConfig>,
pub logo: LogoConfig,
pub default_theme: String,
pub media_directory: String,
pub mail: Option<MailConfig>,
pub ldap: Option<LdapConfig>,
pub proxy: Option<ProxyConfig>,
}
@@ -31,21 +35,21 @@ impl Config {
}
#[derive(Debug, Clone)]
pub enum RocketError {
InvalidEnv,
InvalidAddress,
InvalidSecretKey,
pub enum InvalidRocketConfig {
Env,
Address,
SecretKey,
}
fn get_rocket_config() -> Result<RocketConfig, RocketError> {
let mut c = RocketConfig::active().map_err(|_| RocketError::InvalidEnv)?;
fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
let mut c = RocketConfig::active().map_err(|_| InvalidRocketConfig::Env)?;
let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned());
let port = var("ROCKET_PORT")
.ok()
.map(|s| s.parse::<u16>().unwrap())
.unwrap_or(7878);
let secret_key = var("ROCKET_SECRET_KEY").map_err(|_| RocketError::InvalidSecretKey)?;
let secret_key = var("ROCKET_SECRET_KEY").map_err(|_| InvalidRocketConfig::SecretKey)?;
let form_size = var("FORM_SIZE")
.unwrap_or_else(|_| "128".to_owned())
.parse::<u64>()
@@ -56,10 +60,10 @@ fn get_rocket_config() -> Result<RocketConfig, RocketError> {
.unwrap();
c.set_address(address)
.map_err(|_| RocketError::InvalidAddress)?;
.map_err(|_| InvalidRocketConfig::Address)?;
c.set_port(port);
c.set_secret_key(secret_key)
.map_err(|_| RocketError::InvalidSecretKey)?;
.map_err(|_| InvalidRocketConfig::SecretKey)?;
c.set_limits(
Limits::new()
@@ -155,7 +159,7 @@ impl Default for LogoConfig {
.ok()
.or_else(|| custom_main.clone());
let other = if let Some(main) = custom_main.clone() {
let ext = |path: &str| match path.rsplitn(2, '.').next() {
let ext = |path: &str| match path.rsplit_once('.').map(|x| x.1) {
Some("png") => Some("image/png".to_owned()),
Some("jpg") | Some("jpeg") => Some("image/jpeg".to_owned()),
Some("svg") => Some("image/svg+xml".to_owned()),
@@ -245,6 +249,31 @@ impl SearchTokenizerConfig {
}
}
pub struct MailConfig {
pub server: String,
pub port: u16,
pub helo_name: String,
pub username: String,
pub password: String,
}
fn get_mail_config() -> Option<MailConfig> {
Some(MailConfig {
server: env::var("MAIL_SERVER").ok()?,
port: env::var("MAIL_PORT").map_or(SUBMISSIONS_PORT, |port| match port.as_str() {
"smtp" => SMTP_PORT,
"submissions" => SUBMISSIONS_PORT,
"submission" => SUBMISSION_PORT,
number => number
.parse()
.expect(r#"MAIL_PORT must be "smtp", "submissions", "submission" or an integer."#),
}),
helo_name: env::var("MAIL_HELO_NAME").unwrap_or_else(|_| "localhost".to_owned()),
username: env::var("MAIL_USER").ok()?,
password: env::var("MAIL_PASSWORD").ok()?,
})
}
pub struct LdapConfig {
pub addr: String,
pub base_dn: String,
@@ -335,6 +364,7 @@ lazy_static! {
s.parse::<u32>()
.expect("Couldn't parse DB_MIN_IDLE into u32")
)),
signup: var("SIGNUP").map_or(SignupStrategy::default(), |s| s.parse().unwrap()),
#[cfg(feature = "postgres")]
database_url: var("DATABASE_URL")
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)),
@@ -347,6 +377,7 @@ lazy_static! {
default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()),
media_directory: var("MEDIA_UPLOAD_DIRECTORY")
.unwrap_or_else(|_| "static/media".to_owned()),
mail: get_mail_config(),
ldap: get_ldap_config(),
proxy: get_proxy_config(),
};
+143
View File
@@ -0,0 +1,143 @@
use crate::{
db_conn::DbConn,
schema::email_signups,
users::{NewUser, Role, User},
Error, Result,
};
use chrono::{offset::Utc, Duration, NaiveDateTime};
use diesel::{
Connection as _, ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, RunQueryDsl,
};
use plume_common::utils::random_hex;
use std::ops::Deref;
const TOKEN_VALIDITY_HOURS: i64 = 2;
#[repr(transparent)]
pub struct Token(String);
impl From<String> for Token {
fn from(string: String) -> Self {
Token(string)
}
}
impl From<Token> for String {
fn from(token: Token) -> Self {
token.0
}
}
impl Deref for Token {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Token {
fn generate() -> Self {
Self(random_hex())
}
}
#[derive(Identifiable, Queryable)]
pub struct EmailSignup {
pub id: i32,
pub email: String,
pub token: String,
pub expiration_date: NaiveDateTime,
}
#[derive(Insertable)]
#[table_name = "email_signups"]
pub struct NewEmailSignup<'a> {
pub email: &'a str,
pub token: &'a str,
pub expiration_date: NaiveDateTime,
}
impl EmailSignup {
pub fn start(conn: &DbConn, email: &str) -> Result<Token> {
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, email)?;
let _rows = Self::delete_existings_by_email(conn, email)?;
let token = Token::generate();
let expiration_date = Utc::now()
.naive_utc()
.checked_add_signed(Duration::hours(TOKEN_VALIDITY_HOURS))
.expect("could not calculate expiration date");
let new_signup = NewEmailSignup {
email,
token: &token,
expiration_date,
};
let _rows = diesel::insert_into(email_signups::table)
.values(new_signup)
.execute(&**conn)?;
Ok(token)
})
}
pub fn find_by_token(conn: &DbConn, token: Token) -> Result<Self> {
let signup = email_signups::table
.filter(email_signups::token.eq(token.as_str()))
.first::<Self>(&**conn)
.map_err(Error::from)?;
Ok(signup)
}
pub fn confirm(&self, conn: &DbConn) -> Result<()> {
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
if self.expired() {
Self::delete_existings_by_email(conn, &self.email)?;
return Err(Error::Expired);
}
Ok(())
})
}
pub fn complete(&self, conn: &DbConn, username: String, password: String) -> Result<User> {
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
let user = NewUser::new_local(
conn,
username,
"".to_string(),
Role::Normal,
"",
self.email.clone(),
Some(User::hash_pass(&password)?),
)?;
self.delete(conn)?;
Ok(user)
})
}
fn delete(&self, conn: &DbConn) -> Result<()> {
let _rows = diesel::delete(self).execute(&**conn).map_err(Error::from)?;
Ok(())
}
fn ensure_user_not_exist_by_email(conn: &DbConn, email: &str) -> Result<()> {
if User::email_used(conn, email)? {
let _rows = Self::delete_existings_by_email(conn, email)?;
return Err(Error::UserAlreadyExists);
}
Ok(())
}
fn delete_existings_by_email(conn: &DbConn, email: &str) -> Result<usize> {
let existing_signups = email_signups::table.filter(email_signups::email.eq(email));
diesel::delete(existing_signups)
.execute(&**conn)
.map_err(Error::from)
}
fn expired(&self) -> bool {
self.expiration_date < Utc::now().naive_utc()
}
}
+7 -3
View File
@@ -1,6 +1,6 @@
use crate::{
ap_url, db_conn::DbConn, notifications::*, schema::follows, users::User, Connection, Error,
Result, CONFIG,
ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User,
Connection, Error, Result, CONFIG,
};
use activitypub::activity::{Accept, Follow as FollowAct, Undo};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
@@ -183,6 +183,10 @@ impl FromId<DbConn> for Follow {
.map_err(|(_, e)| e)?;
Follow::accept_follow(conn, &actor, &target, follow, actor.id, target.id)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl AsObject<User, Undo, &DbConn> for Follow {
@@ -195,7 +199,7 @@ impl AsObject<User, Undo, &DbConn> for Follow {
diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any
if let Ok(notif) = Notification::find(&conn, notification_kind::FOLLOW, self.id) {
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
diesel::delete(&notif).execute(&**conn)?;
}
+43 -2
View File
@@ -3,11 +3,12 @@ use crate::{
medias::Media,
safe_string::SafeString,
schema::{instances, users},
users::{Role, User},
users::{NewUser, Role, User},
Connection, Error, Result,
};
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl};
use once_cell::sync::OnceCell;
use plume_common::utils::md_to_html;
use std::sync::RwLock;
@@ -45,6 +46,9 @@ lazy_static! {
static ref LOCAL_INSTANCE: RwLock<Option<Instance>> = RwLock::new(None);
}
const LOCAL_INSTANCE_USERNAME: &str = "__instance__";
static LOCAL_INSTANCE_USER: OnceCell<User> = OnceCell::new();
impl Instance {
pub fn set_local(self) {
LOCAL_INSTANCE.write().unwrap().replace(self);
@@ -76,6 +80,42 @@ impl Instance {
.map_err(Error::from)
}
pub fn create_local_instance_user(conn: &Connection) -> Result<User> {
let instance = Instance::get_local()?;
let email = format!("{}@{}", LOCAL_INSTANCE_USERNAME, &instance.public_domain);
NewUser::new_local(
conn,
LOCAL_INSTANCE_USERNAME.into(),
instance.public_domain,
Role::Instance,
"Local instance",
email,
None,
)
}
pub fn get_local_instance_user() -> Option<&'static User> {
LOCAL_INSTANCE_USER.get()
}
pub fn get_local_instance_user_uncached(conn: &Connection) -> Result<User> {
users::table
.filter(users::role.eq(3))
.first(conn)
.or_else(|err| match err {
NotFound => Self::create_local_instance_user(conn),
_ => Err(Error::Db(err)),
})
}
pub fn cache_local_instance_user(conn: &Connection) {
let _ = LOCAL_INSTANCE_USER.get_or_init(|| {
Self::get_local_instance_user_uncached(conn)
.or_else(|_| Self::create_local_instance_user(conn))
.expect("Failed to cache local instance user")
});
}
pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<Instance>> {
instances::table
.order(instances::public_domain.asc())
@@ -304,6 +344,7 @@ pub(crate) mod tests {
})
.collect();
Instance::cache_local(conn);
Instance::cache_local_instance_user(conn);
res
}
Executable → Regular
+45 -8
View File
@@ -1,4 +1,3 @@
#![feature(try_trait)]
#![feature(never_type)]
#![feature(proc_macro_hygiene)]
#![feature(box_patterns)]
@@ -17,8 +16,10 @@ extern crate serde_json;
#[macro_use]
extern crate tantivy;
pub use lettre;
pub use lettre::smtp;
use once_cell::sync::Lazy;
use plume_common::activity_pub::inbox::InboxError;
use plume_common::activity_pub::{inbox::InboxError, request, sign};
use posts::PostEvent;
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
use users::UserEvent;
@@ -66,6 +67,7 @@ pub enum Error {
Url,
Webfinger,
Expired,
UserAlreadyExists,
}
impl From<bcrypt::BcryptError> for Error {
@@ -80,15 +82,15 @@ impl From<openssl::error::ErrorStack> for Error {
}
}
impl From<diesel::result::Error> for Error {
fn from(err: diesel::result::Error) -> Self {
Error::Db(err)
impl From<sign::Error> for Error {
fn from(_: sign::Error) -> Self {
Error::Signature
}
}
impl From<std::option::NoneError> for Error {
fn from(_: std::option::NoneError) -> Self {
Error::NotFound
impl From<diesel::result::Error> for Error {
fn from(err: diesel::result::Error) -> Self {
Error::Db(err)
}
}
@@ -158,6 +160,12 @@ impl From<InboxError<Error>> for Error {
}
}
impl From<request::Error> for Error {
fn from(_err: request::Error) -> Error {
Error::Request
}
}
pub type Result<T> = std::result::Result<T, Error>;
/// Adds a function to a model, that returns the first
@@ -295,6 +303,33 @@ pub fn ap_url(url: &str) -> String {
format!("https://{}", url)
}
pub trait SmtpNewWithAddr {
fn new_with_addr(
addr: (&str, u16),
) -> std::result::Result<smtp::SmtpClient, smtp::error::Error>;
}
impl SmtpNewWithAddr for smtp::SmtpClient {
// Stolen from lettre::smtp::SmtpClient::new_simple()
fn new_with_addr(addr: (&str, u16)) -> std::result::Result<Self, smtp::error::Error> {
use native_tls::TlsConnector;
use smtp::{
client::net::{ClientTlsParameters, DEFAULT_TLS_PROTOCOLS},
ClientSecurity, SmtpClient,
};
let (domain, port) = addr;
let mut tls_builder = TlsConnector::builder();
tls_builder.min_protocol_version(Some(DEFAULT_TLS_PROTOCOLS[0]));
let tls_parameters =
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
SmtpClient::new((domain, port), ClientSecurity::Wrapper(tls_parameters))
}
}
#[cfg(test)]
#[macro_use]
mod tests {
@@ -342,6 +377,7 @@ pub mod blogs;
pub mod comment_seers;
pub mod comments;
pub mod db_conn;
pub mod email_signups;
pub mod follows;
pub mod headers;
pub mod inbox;
@@ -362,6 +398,7 @@ pub mod safe_string;
#[allow(unused_imports)]
pub mod schema;
pub mod search;
pub mod signups;
pub mod tags;
pub mod timeline;
pub mod users;
+8 -3
View File
@@ -1,12 +1,13 @@
use crate::{
db_conn::DbConn, notifications::*, posts::Post, schema::likes, timeline::*, users::User,
Connection, Error, Result, CONFIG,
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*,
users::User, Connection, Error, Result, CONFIG,
};
use activitypub::activity;
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY,
};
@@ -137,6 +138,10 @@ impl FromId<DbConn> for Like {
res.notify(conn)?;
Ok(res)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl AsObject<User, activity::Undo, &DbConn> for Like {
@@ -148,7 +153,7 @@ impl AsObject<User, activity::Undo, &DbConn> for Like {
diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any
if let Ok(notif) = Notification::find(&conn, notification_kind::LIKE, self.id) {
if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
diesel::delete(&notif).execute(&**conn)?;
}
Ok(())
+1
View File
@@ -143,6 +143,7 @@ macro_rules! func {
}
}
#[allow(dead_code)]
#[derive(Clone, Queryable, Identifiable)]
struct ListElem {
pub id: i32,
+35 -26
View File
@@ -3,12 +3,11 @@ use crate::{
users::User, Connection, Error, Result, CONFIG,
};
use activitypub::object::Image;
use askama_escape::escape;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use guid_create::GUID;
use plume_common::{
activity_pub::{inbox::FromId, Id},
utils::MediaProcessor,
activity_pub::{inbox::FromId, request, Id},
utils::{escape, MediaProcessor},
};
use std::{
fs::{self, DirBuilder},
@@ -104,8 +103,8 @@ impl Media {
pub fn category(&self) -> MediaCategory {
match &*self
.file_path
.rsplitn(2, '.')
.next()
.rsplit_once('.')
.map(|x| x.1)
.expect("Media::category: extension error")
.to_lowercase()
{
@@ -156,7 +155,11 @@ impl Media {
if self.is_remote {
Ok(self.remote_url.clone().unwrap_or_default())
} else {
let file_path = self.file_path.replace(path::MAIN_SEPARATOR, "/");
let file_path = self.file_path.replace(path::MAIN_SEPARATOR, "/").replacen(
&CONFIG.media_directory,
"static/media",
1,
); // "static/media" from plume::routs::plume_media_files()
Ok(ap_url(&format!(
"{}/{}",
Instance::get_local()?.public_domain,
@@ -204,31 +207,33 @@ impl Media {
// TODO: merge with save_remote?
pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> {
let remote_url = image.object_props.url_string().ok()?;
let remote_url = image
.object_props
.url_string()
.or(Err(Error::MissingApProperty))?;
let path = determine_mirror_file_path(&remote_url);
let parent = path.parent()?;
let parent = path.parent().ok_or(Error::InvalidValue)?;
if !parent.is_dir() {
DirBuilder::new().recursive(true).create(parent)?;
}
let mut dest = fs::File::create(path.clone()).ok()?;
let mut dest = fs::File::create(path.clone())?;
// TODO: conditional GET
if let Some(proxy) = CONFIG.proxy() {
reqwest::ClientBuilder::new().proxy(proxy.clone()).build()?
} else {
reqwest::Client::new()
}
.get(remote_url.as_str())
.send()
.ok()?
.copy_to(&mut dest)
.ok()?;
request::get(
remote_url.as_str(),
User::get_sender(),
CONFIG.proxy().cloned(),
)?
.copy_to(&mut dest)?;
Media::find_by_file_path(conn, &path.to_str()?)
Media::find_by_file_path(conn, path.to_str().ok_or(Error::InvalidValue)?)
.and_then(|mut media| {
let mut updated = false;
let alt_text = image.object_props.content_string().ok()?;
let alt_text = image
.object_props
.content_string()
.or(Err(Error::NotFound))?;
let sensitive = image.object_props.summary_string().is_ok();
let content_warning = image.object_props.summary_string().ok();
if media.alt_text != alt_text {
@@ -260,8 +265,11 @@ impl Media {
Media::insert(
conn,
NewMedia {
file_path: path.to_str()?.to_string(),
alt_text: image.object_props.content_string().ok()?,
file_path: path.to_str().ok_or(Error::InvalidValue)?.to_string(),
alt_text: image
.object_props
.content_string()
.or(Err(Error::NotFound))?,
is_remote: false,
remote_url: None,
sensitive: image.object_props.summary_string().is_ok(),
@@ -271,9 +279,10 @@ impl Media {
image
.object_props
.attributed_to_link_vec::<Id>()
.ok()?
.or(Err(Error::NotFound))?
.into_iter()
.next()?
.next()
.ok_or(Error::NotFound)?
.as_ref(),
None,
CONFIG.proxy(),
@@ -321,7 +330,7 @@ fn determine_mirror_file_path(url: &str) -> PathBuf {
.next()
.map(ToOwned::to_owned)
.unwrap_or_else(|| String::from("png"));
file_path.push(format!("{}.{}", GUID::rand().to_string(), ext));
file_path.push(format!("{}.{}", GUID::rand(), ext));
});
file_path
}
+6 -2
View File
@@ -47,7 +47,11 @@ impl Mention {
pub fn get_user(&self, conn: &Connection) -> Result<User> {
match self.get_post(conn) {
Ok(p) => Ok(p.get_authors(conn)?.into_iter().next()?),
Ok(p) => Ok(p
.get_authors(conn)?
.into_iter()
.next()
.ok_or(Error::NotFound)?),
Err(_) => self.get_comment(conn).and_then(|c| c.get_author(conn)),
}
}
@@ -77,7 +81,7 @@ impl Mention {
in_post: bool,
notify: bool,
) -> Result<Self> {
let ap_url = ment.link_props.href_string().ok()?;
let ap_url = ment.link_props.href_string().or(Err(Error::NotFound))?;
let mentioned = User::find_by_ap_url(conn, &ap_url)?;
if in_post {
+2 -1
View File
@@ -105,7 +105,8 @@ impl ImportedMigrations {
pub fn rerun_last_migration(&self, conn: &Connection, path: &Path) -> Result<()> {
let latest_migration = conn.latest_run_migration_version()?;
let id = latest_migration
.and_then(|m| self.0.binary_search_by_key(&m.as_str(), |m| m.name).ok())?;
.and_then(|m| self.0.binary_search_by_key(&m.as_str(), |m| m.name).ok())
.ok_or(Error::NotFound)?;
let migration = &self.0[id];
conn.transaction(|| {
migration.revert(conn, path)?;
+1 -1
View File
@@ -61,7 +61,7 @@ impl PasswordResetRequest {
}
pub fn find_and_delete_by_token(conn: &Connection, token: &str) -> Result<Self> {
let request = Self::find_by_token(&conn, &token)?;
let request = Self::find_by_token(conn, token)?;
let filter =
password_reset_requests::table.filter(password_reset_requests::id.eq(request.id));
+41 -24
View File
@@ -10,15 +10,15 @@ use activitypub::{
CustomObject,
};
use chrono::{NaiveDateTime, TimeZone, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use heck::KebabCase;
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl};
use once_cell::sync::Lazy;
use plume_common::{
activity_pub::{
inbox::{AsActor, AsObject, FromId},
sign::Signer,
Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY,
},
utils::md_to_html,
utils::{iri_percent_encode_seg, md_to_html},
};
use riker::actors::{Publish, Tell};
use std::collections::{HashMap, HashSet};
@@ -67,20 +67,15 @@ impl Post {
find_by!(posts, find_by_ap_url, ap_url as &str);
last!(posts);
pub fn insert(conn: &Connection, new: NewPost) -> Result<Self> {
pub fn insert(conn: &Connection, mut new: NewPost) -> Result<Self> {
if new.ap_url.is_empty() {
let blog = Blog::get(conn, new.blog_id)?;
new.ap_url = Self::ap_url(blog, &new.slug);
}
diesel::insert_into(posts::table)
.values(new)
.execute(conn)?;
let mut post = Self::last(conn)?;
if post.ap_url.is_empty() {
post.ap_url = ap_url(&format!(
"{}/~/{}/{}/",
CONFIG.base_url,
post.get_blog(conn)?.fqn,
post.slug
));
let _: Post = post.save_changes(conn)?;
}
let post = Self::last(conn)?;
if post.published {
post.publish_published();
@@ -103,7 +98,7 @@ impl Post {
}
pub fn delete(&self, conn: &Connection) -> Result<()> {
for m in Mention::list_for_post(&conn, self.id)? {
for m in Mention::list_for_post(conn, self.id)? {
m.delete(conn)?;
}
diesel::delete(self).execute(conn)?;
@@ -254,6 +249,20 @@ impl Post {
.map_err(Error::from)
}
pub fn ap_url(blog: Blog, slug: &str) -> String {
ap_url(&format!(
"{}/~/{}/{}/",
CONFIG.base_url,
blog.fqn,
iri_percent_encode_seg(slug)
))
}
// It's better to calc slug in insert and update
pub fn slug(title: &str) -> &str {
title
}
pub fn get_authors(&self, conn: &Connection) -> Result<Vec<User>> {
use crate::schema::post_authors;
use crate::schema::users;
@@ -449,14 +458,14 @@ impl Post {
.filter_map(|(id, m)| id.map(|id| (m, id)))
.collect::<Vec<_>>();
let old_mentions = Mention::list_for_post(&conn, self.id)?;
let old_mentions = Mention::list_for_post(conn, self.id)?;
let old_user_mentioned = old_mentions
.iter()
.map(|m| m.mentioned_id)
.collect::<HashSet<_>>();
for (m, id) in &mentions {
if !old_user_mentioned.contains(&id) {
Mention::from_activity(&*conn, &m, self.id, true, true)?;
if !old_user_mentioned.contains(id) {
Mention::from_activity(&*conn, m, self.id, true, true)?;
}
}
@@ -468,7 +477,7 @@ impl Post {
.iter()
.filter(|m| !new_mentions.contains(&m.mentioned_id))
{
m.delete(&conn)?;
m.delete(conn)?;
}
Ok(())
}
@@ -649,12 +658,12 @@ impl FromId<DbConn> for Post {
.and_then(|mut post| {
let mut updated = false;
let slug = title.to_kebab_case();
let slug = Self::slug(&title);
let content = SafeString::new(&article.object_props.content_string()?);
let subtitle = article.object_props.summary_string()?;
let source = article.ap_object_props.source_object::<Source>()?.content;
if post.slug != slug {
post.slug = slug;
post.slug = slug.to_string();
updated = true;
}
if post.title != title {
@@ -692,8 +701,8 @@ impl FromId<DbConn> for Post {
Post::insert(
conn,
NewPost {
blog_id: blog?.id,
slug: title.to_kebab_case(),
blog_id: blog.ok_or(Error::NotFound)?.id,
slug: Self::slug(&title).to_string(),
title,
content: SafeString::new(&article.object_props.content_string()?),
published: true,
@@ -751,6 +760,10 @@ impl FromId<DbConn> for Post {
Ok(post)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl AsObject<User, Create, &DbConn> for Post {
@@ -822,6 +835,10 @@ impl FromId<DbConn> for PostUpdate {
tags: updated.object.object_props.tag,
})
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl AsObject<User, Update, &DbConn> for PostUpdate {
@@ -838,7 +855,7 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
}
if let Some(title) = self.title {
post.slug = title.to_kebab_case();
post.slug = Post::slug(&title).to_string();
post.title = title;
}
+8 -3
View File
@@ -1,12 +1,13 @@
use crate::{
db_conn::DbConn, notifications::*, posts::Post, schema::reshares, timeline::*, users::User,
Connection, Error, Result, CONFIG,
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares,
timeline::*, users::User, Connection, Error, Result, CONFIG,
};
use activitypub::activity::{Announce, Undo};
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY,
};
@@ -162,6 +163,10 @@ impl FromId<DbConn> for Reshare {
res.notify(conn)?;
Ok(res)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl AsObject<User, Undo, &DbConn> for Reshare {
@@ -173,7 +178,7 @@ impl AsObject<User, Undo, &DbConn> for Reshare {
diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any
if let Ok(notif) = Notification::find(&conn, notification_kind::RESHARE, self.id) {
if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) {
diesel::delete(&notif).execute(&**conn)?;
}
+1 -1
View File
@@ -102,7 +102,7 @@ pub struct SafeString {
impl SafeString {
pub fn new(value: &str) -> Self {
SafeString {
value: CLEAN.clean(&value).to_string(),
value: CLEAN.clean(value).to_string(),
}
}
+11
View File
@@ -73,6 +73,7 @@ table! {
user_id -> Int4,
}
}
table! {
email_blocklist(id){
id -> Int4,
@@ -83,6 +84,15 @@ table! {
}
}
table! {
email_signups (id) {
id -> Int4,
email -> Varchar,
token -> Varchar,
expiration_date -> Timestamp,
}
}
table! {
follows (id) {
id -> Int4,
@@ -306,6 +316,7 @@ allow_tables_to_appear_in_same_query!(
blogs,
comments,
comment_seers,
email_signups,
follows,
instances,
likes,
+2 -2
View File
@@ -148,7 +148,7 @@ impl PlumeQuery {
/// Parse a query string into this Query
pub fn parse_query(&mut self, query: &str) -> &mut Self {
self.from_str_req(&query.trim())
self.from_str_req(query.trim())
}
/// Convert this Query to a Tantivy Query
@@ -360,7 +360,7 @@ impl std::str::FromStr for PlumeQuery {
fn from_str(query: &str) -> Result<PlumeQuery, !> {
let mut res: PlumeQuery = Default::default();
res.from_str_req(&query.trim());
res.from_str_req(query.trim());
Ok(res)
}
}
+72
View File
@@ -0,0 +1,72 @@
use crate::CONFIG;
use rocket::request::{FromRequest, Outcome, Request};
use std::fmt;
use std::str::FromStr;
pub enum Strategy {
Password,
Email,
}
impl Default for Strategy {
fn default() -> Self {
Self::Password
}
}
impl FromStr for Strategy {
type Err = StrategyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use self::Strategy::*;
match s {
"password" => Ok(Password),
"email" => Ok(Email),
s => Err(StrategyError::Unsupported(s.to_string())),
}
}
}
#[derive(Debug)]
pub enum StrategyError {
Unsupported(String),
}
impl fmt::Display for StrategyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use self::StrategyError::*;
match self {
// FIXME: Calc option strings from enum
Unsupported(s) => write!(f, "Unsupported strategy: {}. Choose password or email", s),
}
}
}
impl std::error::Error for StrategyError {}
pub struct Password();
pub struct Email();
impl<'a, 'r> FromRequest<'a, 'r> for Password {
type Error = ();
fn from_request(_request: &'a Request<'r>) -> Outcome<Self, ()> {
match matches!(CONFIG.signup, Strategy::Password) {
true => Outcome::Success(Self()),
false => Outcome::Forward(()),
}
}
}
impl<'a, 'r> FromRequest<'a, 'r> for Email {
type Error = ();
fn from_request(_request: &'a Request<'r>) -> Outcome<Self, ()> {
match matches!(CONFIG.signup, Strategy::Email) {
true => Outcome::Success(Self()),
false => Outcome::Forward(()),
}
}
}
+13
View File
@@ -223,6 +223,9 @@ impl Timeline {
}
pub fn add_post(&self, conn: &Connection, post: &Post) -> Result<()> {
if self.includes_post(conn, post)? {
return Ok(());
}
diesel::insert_into(timeline::table)
.values(TimelineEntry {
post_id: post.id,
@@ -236,6 +239,16 @@ impl Timeline {
let query = TimelineQuery::parse(&self.query)?;
query.matches(conn, self, post, kind)
}
fn includes_post(&self, conn: &Connection, post: &Post) -> Result<bool> {
diesel::dsl::select(diesel::dsl::exists(
timeline::table
.filter(timeline::timeline_id.eq(self.id))
.filter(timeline::post_id.eq(post.id)),
))
.get_result(conn)
.map_err(Error::from)
}
}
#[cfg(test)]
+49 -30
View File
@@ -18,12 +18,6 @@ pub enum QueryError {
RuntimeError(String),
}
impl From<std::option::NoneError> for QueryError {
fn from(_: std::option::NoneError) -> Self {
QueryError::UnexpectedEndOfQuery
}
}
pub type QueryResult<T> = std::result::Result<T, QueryError>;
#[derive(Debug, Clone, Copy, PartialEq)]
@@ -239,7 +233,7 @@ impl WithList {
) -> Result<bool> {
match list {
List::List(name) => {
let list = lists::List::find_for_user_by_name(conn, timeline.user_id, &name)?;
let list = lists::List::find_for_user_by_name(conn, timeline.user_id, name)?;
match (self, list.kind()) {
(WithList::Blog, ListType::Blog) => list.contains_blog(conn, post.blog_id),
(WithList::Author { boosts, likes }, ListType::User) => match kind {
@@ -414,7 +408,7 @@ enum List<'a> {
fn parse_s<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> {
let mut res = Vec::new();
let (left, token) = parse_a(&stream)?;
let (left, token) = parse_a(stream)?;
res.push(token);
stream = left;
while !stream.is_empty() {
@@ -436,7 +430,7 @@ fn parse_s<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
fn parse_a<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> {
let mut res = Vec::new();
let (left, token) = parse_b(&stream)?;
let (left, token) = parse_b(stream)?;
res.push(token);
stream = left;
while !stream.is_empty() {
@@ -463,7 +457,7 @@ fn parse_b<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<
match left.get(0) {
Some(Token::RParent(_)) => Ok((&left[1..], token)),
Some(t) => t.get_error(Token::RParent(0)),
None => None?,
None => Err(QueryError::UnexpectedEndOfQuery),
}
}
_ => parse_c(stream),
@@ -484,9 +478,13 @@ fn parse_c<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<
}
fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Arg<'a>)> {
match stream.get(0).map(Token::get_text)? {
match stream
.get(0)
.map(Token::get_text)
.ok_or(QueryError::UnexpectedEndOfQuery)?
{
s @ "blog" | s @ "author" | s @ "license" | s @ "tags" | s @ "lang" => {
match stream.get(1)? {
match stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)? {
Token::Word(_, _, r#in) if r#in == &"in" => {
let (mut left, list) = parse_l(&stream[2..])?;
let kind = match s {
@@ -498,7 +496,12 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
if *clude != "include" && *clude != "exclude" {
break;
}
match (*clude, left.get(1).map(Token::get_text)?) {
match (
*clude,
left.get(1)
.map(Token::get_text)
.ok_or(QueryError::UnexpectedEndOfQuery)?,
) {
("include", "reshares") | ("include", "reshare") => {
boosts = true
}
@@ -529,7 +532,10 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
t => t.get_error(Token::Word(0, 0, "'in'")),
}
}
s @ "title" | s @ "subtitle" | s @ "content" => match (stream.get(1)?, stream.get(2)?) {
s @ "title" | s @ "subtitle" | s @ "content" => match (
stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)?,
stream.get(2).ok_or(QueryError::UnexpectedEndOfQuery)?,
) {
(Token::Word(_, _, contains), Token::Word(_, _, w)) if contains == &"contains" => Ok((
&stream[3..],
Arg::Contains(
@@ -555,7 +561,13 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
if *clude != "include" && *clude != "exclude" {
break;
}
match (*clude, stream.get(2).map(Token::get_text)?) {
match (
*clude,
stream
.get(2)
.map(Token::get_text)
.ok_or(QueryError::UnexpectedEndOfQuery)?,
) {
("include", "reshares") | ("include", "reshare") => boosts = true,
("exclude", "reshares") | ("exclude", "reshare") => boosts = false,
("include", "likes") | ("include", "like") => likes = true,
@@ -577,20 +589,23 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
"all" => Ok((&stream[1..], Arg::Boolean(Bool::All))),
_ => unreachable!(),
},
_ => stream.get(0)?.get_error(Token::Word(
0,
0,
"one of 'blog', 'author', 'license', 'tags', 'lang', \
_ => stream
.get(0)
.ok_or(QueryError::UnexpectedEndOfQuery)?
.get_error(Token::Word(
0,
0,
"one of 'blog', 'author', 'license', 'tags', 'lang', \
'title', 'subtitle', 'content', 'followed', 'has_cover', 'local' or 'all'",
)),
)),
}
}
fn parse_l<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], List<'a>)> {
match stream.get(0)? {
match stream.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? {
Token::LBracket(_) => {
let (left, list) = parse_m(&stream[1..])?;
match left.get(0)? {
match left.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? {
Token::RBracket(_) => Ok((&left[1..], List::Array(list))),
t => t.get_error(Token::Word(0, 0, "one of ']' or ','")),
}
@@ -601,16 +616,20 @@ fn parse_l<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Lis
}
fn parse_m<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Vec<&'a str>)> {
let mut res: Vec<&str> = vec![match stream.get(0)? {
Token::Word(_, _, w) => w,
t => return t.get_error(Token::Word(0, 0, "any word")),
}];
stream = &stream[1..];
while let Token::Comma(_) = stream[0] {
res.push(match stream.get(1)? {
let mut res: Vec<&str> = vec![
match stream.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? {
Token::Word(_, _, w) => w,
t => return t.get_error(Token::Word(0, 0, "any word")),
});
},
];
stream = &stream[1..];
while let Token::Comma(_) = stream[0] {
res.push(
match stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)? {
Token::Word(_, _, w) => w,
t => return t.get_error(Token::Word(0, 0, "any word")),
},
);
stream = &stream[2..];
}
+57 -79
View File
@@ -22,17 +22,13 @@ use openssl::{
};
use plume_common::{
activity_pub::{
ap_accept_header,
inbox::{AsActor, AsObject, FromId},
sign::{gen_keypair, Signer},
request::get,
sign::{gen_keypair, Error as SignError, Result as SignResult, Signer},
ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
},
utils,
};
use reqwest::{
header::{HeaderValue, ACCEPT},
ClientBuilder,
};
use riker::actors::{Publish, Tell};
use rocket::{
outcome::IntoOutcome,
@@ -52,6 +48,7 @@ pub enum Role {
Admin = 0,
Moderator = 1,
Normal = 2,
Instance = 3,
}
#[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)]
@@ -78,6 +75,7 @@ pub struct User {
pub summary_html: SafeString,
/// 0 = admin
/// 1 = moderator
/// 3 = local instance
/// anything else = normal user
pub role: i32,
pub preferred_theme: Option<String>,
@@ -204,13 +202,35 @@ impl User {
}
}
/**
* TODO: Should create user record with normalized(lowercased) email
*/
pub fn email_used(conn: &DbConn, email: &str) -> Result<bool> {
use diesel::dsl::{exists, select};
select(exists(
users::table
.filter(users::instance_id.eq(Instance::get_local()?.id))
.filter(users::email.eq(email))
.or_filter(users::email.eq(email.to_ascii_lowercase())),
))
.get_result(&**conn)
.map_err(Error::from)
}
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<User> {
let link = resolve(acct.to_owned(), true)?
.links
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger)?;
User::from_id(conn, link.href.as_ref()?, None, CONFIG.proxy()).map_err(|(_, e)| e)
User::from_id(
conn,
link.href.as_ref().ok_or(Error::Webfinger)?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)
}
pub fn fetch_remote_interact_uri(acct: &str) -> Result<String> {
@@ -223,20 +243,7 @@ impl User {
}
fn fetch(url: &str) -> Result<CustomPerson> {
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(url)
.header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?;
let text = &res.text()?;
// without this workaround, publicKey is not correctly deserialized
let ap_sign = serde_json::from_str::<ApSignature>(text)?;
@@ -255,10 +262,10 @@ impl User {
conn,
json.object
.object_props
.icon_image()?
.icon_image()? // FIXME: Fails when icon is not set
.object_props
.url_string()?,
&self,
self,
)
.ok();
@@ -427,12 +434,12 @@ impl User {
let last = &format!(
"{}?page={}",
&self.outbox_url,
self.get_activities_count(&conn) / i64::from(ITEMS_PER_PAGE) + 1
self.get_activities_count(conn) / i64::from(ITEMS_PER_PAGE) + 1
);
coll.collection_props.set_first_link(Id::new(first))?;
coll.collection_props.set_last_link(Id::new(last))?;
coll.collection_props
.set_total_items_u64(self.get_activities_count(&conn) as u64)?;
.set_total_items_u64(self.get_activities_count(conn) as u64)?;
Ok(ActivityStream::new(coll))
}
pub fn outbox_page(
@@ -441,7 +448,7 @@ impl User {
(min, max): (i32, i32),
) -> Result<ActivityStream<OrderedCollectionPage>> {
let acts = self.get_activities_page(conn, (min, max))?;
let n_acts = self.get_activities_count(&conn);
let n_acts = self.get_activities_count(conn);
let mut coll = OrderedCollectionPage::default();
if n_acts - i64::from(min) >= i64::from(ITEMS_PER_PAGE) {
coll.collection_page_props.set_next_link(Id::new(&format!(
@@ -463,20 +470,7 @@ impl User {
Ok(ActivityStream::new(coll))
}
fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> {
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(url)
.header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?;
let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?;
let items = json["items"]
@@ -490,20 +484,11 @@ impl User {
Ok((items, next))
}
pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(&self.outbox_url[..])
.header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let mut res = get(
&self.outbox_url[..],
Self::get_sender(),
CONFIG.proxy().cloned(),
)?;
let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?;
if let Some(first) = json.get("first") {
@@ -513,7 +498,7 @@ impl User {
if page.is_empty() {
break;
}
items.extend(page.drain(..));
items.append(&mut page);
if let Some(n) = nxt {
if n == next {
break;
@@ -535,20 +520,11 @@ impl User {
}
pub fn fetch_followers_ids(&self) -> Result<Vec<String>> {
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(&self.followers_endpoint[..])
.header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let mut res = get(
&self.followers_endpoint[..],
Self::get_sender(),
CONFIG.proxy().cloned(),
)?;
let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?;
Ok(json["items"]
@@ -720,7 +696,7 @@ impl User {
pub fn get_keypair(&self) -> Result<PKey<Private>> {
PKey::from_rsa(Rsa::private_key_from_pem(
self.private_key.clone()?.as_ref(),
self.private_key.clone().ok_or(Error::Signature)?.as_ref(),
)?)
.map_err(Error::from)
}
@@ -943,7 +919,7 @@ impl FromId<DbConn> for User {
fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> {
let url = Url::parse(&acct.object.object_props.id_string()?)?;
let inst = url.host_str()?;
let inst = url.host_str().ok_or(Error::Url)?;
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
Instance::insert(
conn,
@@ -1031,6 +1007,10 @@ impl FromId<DbConn> for User {
Ok(user)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl AsActor<&DbConn> for User {
@@ -1063,24 +1043,22 @@ impl AsObject<User, Delete, &DbConn> for User {
}
impl Signer for User {
type Error = Error;
fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url)
}
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = self.get_keypair()?;
fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
let key = self.get_keypair().map_err(|_| SignError())?;
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?;
signer.sign_to_vec().map_err(Error::from)
signer.sign_to_vec().map_err(SignError::from)
}
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?;
verifier.verify(&signature).map_err(Error::from)
verifier.verify(signature).map_err(SignError::from)
}
}
@@ -1121,7 +1099,7 @@ impl NewUser {
display_name,
role: role as i32,
summary: summary.to_owned(),
summary_html: SafeString::new(&utils::md_to_html(&summary, None, false, None).0),
summary_html: SafeString::new(&utils::md_to_html(summary, None, false, None).0),
email: Some(email),
hashed_password: password,
instance_id: instance.id,
+12 -12
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Afrikaans\n"
"Language: af_ZA\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""
+12 -12
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Arabic\n"
"Language: ar_SA\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "فتح محرر النصوص الغني"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "العنوان"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "العنوان الثانوي أو الملخص"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "اكتب مقالك هنا. ماركداون مُدَعَّم."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "يتبقا {} حرفا تقريبا"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "الوسوم"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "الرخصة"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "الغلاف"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "هذه مسودة"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "نشر كتابا"
+13 -13
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Bulgarian\n"
"Language: bg_BG\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "Искате ли да активирате локално автоматично запаметяване, последно редактирано в {}?"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Отворете редактора с богат текст"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Заглавие"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Подзаглавие или резюме"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Напишете статията си тук. Поддържа се Markdown."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Остават {} знака вляво"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Етикети"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Лиценз"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Основно изображение"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Това е проект"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Публикувай"
+12 -12
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Catalan\n"
"Language: ca_ES\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Obre leditor de text enriquit"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Títol"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Subtítol o resum"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Escriviu el vostre article ací. Podeu fer servir el Markdown."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Queden uns {} caràcters"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Etiquetes"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Llicència"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Coberta"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Açò és un esborrany"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Publica"
+12 -12
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Czech\n"
"Language: cs_CZ\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Otevřít editor formátovaného textu"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Nadpis"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Podnadpis, nebo shrnutí"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Sem napište svůj článek. Markdown je podporován."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Zbývá kolem {} znaků"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Tagy"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licence"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Titulka"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Tohle je koncept"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Zveřejnit"
+12 -12
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Danish\n"
"Language: da_DK\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""
+13 -13
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "Möchten Sie die lokale automatische Speicherung laden, die zuletzt um {} bearbeitet wurde?"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr " Rich Text Editor (RTE) öffnen"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Titel"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Untertitel oder Zusammenfassung"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Schreiben deinen Artikel hier. Markdown wird unterstützt."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Ungefähr {} Zeichen übrig"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Schlagwörter"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Lizenz"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Einband"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Dies ist ein Entwurf"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Veröffentlichen"
+12 -12
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Greek\n"
"Language: el_GR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""
+12 -12
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""
+12 -12
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Esperanto\n"
"Language: eo_UY\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Malfermi la riĉan redaktilon"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Titolo"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Verku vian artikolon ĉi tie. Markdown estas subtenita."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Proksimume {} signoj restantaj"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Etikedoj"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Permesilo"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Kovro"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Malfinias"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Eldoni"
+13 -13
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "¿Quieres cargar el guardado automático local editado por última vez en {}?"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Abrir el editor de texto enriquecido"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Título"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Subtítulo, o resumen"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Escriba su artículo aquí. Puede utilizar Markdown."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Quedan unos {} caracteres"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Etiquetas"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licencia"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Cubierta"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Esto es un borrador"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Publicar"
+15 -15
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Persian\n"
"Language: fa_IR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "آیا می‌خواهید نسخهٔ ذخیره شدهٔ خودکار محلّی از آخرین ویرایش در {} را بار کنید؟"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "باز کردن ویرایش‌گر غنی"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "عنوان"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "زیرعنوان، یا چکیده"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "مقاله را اینجا بنویسید. از مارک‌داون پشتیبانی می‌شود."
msgstr "مقاله‌تان را اینجا بنویسید. از مارک‌داون پشتیبانی می‌شود."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "نزدیک به {} حرف باقی مانده است"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "برچسب‌ها"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "پروانه"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "تصویر شاخص"
msgstr "جلد"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "این، یک پیش‌نویس است"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "انتشار"
+12 -12
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Finnish\n"
"Language: fi_FI\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Avaa edistynyt tekstieditori"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Otsikko"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Alaotsikko tai tiivistelmä"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Kirjoita artikkelisi tähän. Markdown -kuvauskieli on tuettu."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "%{count} merkkiä jäljellä"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Tagit"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Lisenssi"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Kansi"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Tämä on luonnos"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Julkaise"
+13 -13
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "Voulez vous charger la sauvegarde automatique locale, éditée la dernière fois à {}?"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Ouvrir l'éditeur de texte avancé"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Titre"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Sous-titre ou résumé"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Écrivez votre article ici. Vous pouvez utiliser du Markdown."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Environ {} caractères restant"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Étiquettes"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licence"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Illustration"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Ceci est un brouillon"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Publier"
+13 -13
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Galician\n"
"Language: gl_ES\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "Queres cargar a última copia gardada editada o {}?"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Abre o editor de texto enriquecido"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Título"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Subtítulo, ou resumo"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Escribe aquí o teu artigo: podes utilizar Markdown."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Dispós de {} caracteres"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Etiquetas"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licenza"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Portada"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Este é un borrador"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Publicar"
+12 -12
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Hebrew\n"
"Language: he_IL\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""
+12 -12
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Hindi\n"
"Language: hi_IN\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "शीर्षक"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "अपना आर्टिकल या लेख यहाँ लिखें. Markdown उपलब्ध है."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "लगभग {} अक्षर बाकी हैं"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "टैग्स"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "लाइसेंस"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "पब्लिश करें"
+12 -12
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Croatian\n"
"Language: hr_HR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Naslov"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Tagovi"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licenca"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Objavi"
+12 -12
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Hungarian\n"
"Language: hu_HU\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""
+12 -12
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Apri il compositore di testo avanzato"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Titolo"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Sottotitolo, o sommario"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Scrivi qui il tuo articolo. È supportato il Markdown."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Circa {} caratteri rimasti"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Etichette"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licenza"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Copertina"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Questa è una bozza"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Pubblica"
+12 -12
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"Language: ja_JP\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "リッチテキストエディターを開く"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "タイトル"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "サブタイトル、または概要"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "投稿をここに書きます。Markdown がサポートされています。"
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "残り約 {} 文字"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "タグ"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "ライセンス"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "カバー"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "これは下書きです"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "公開"
+12 -12
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Korean\n"
"Language: ko_KR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""
+13 -13
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "Wilt u de lokale auto-opslaan laden? Laatst bewerkt om: {}"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Open de rich-text editor"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Titel"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Ondertitel of samenvatting"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Schrijf hier je artikel. Markdown wordt ondersteund."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Ongeveer {} tekens over"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Tags"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licentie"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Hoofdafbeelding"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Dit is een concept"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Publiceren"
+14 -14
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Norwegian\n"
"Language: no_NO\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
msgstr "Lisens"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""
msgstr "Publiser"
+12 -12
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Polish\n"
"Language: pl_PL\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Otwórz edytor tekstu sformatowanego"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Tytuł"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Podtytuł, lub podsumowanie"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Napisz swój artykuł tutaj. Markdown jest obsługiwany."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Pozostało w okolicy {} znaków"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Tagi"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licencja"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Okładka"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "To jest szkic"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Publikuj"
+11 -11
View File
@@ -12,46 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
# plume-front/src/editor.rs:188
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:281
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:314
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:318
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:325
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:336
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:413
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:417
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:437
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:444
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""
+13 -13
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Portuguese, Brazilian\n"
"Language: pt_BR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "Você quer carregar o último conteúdo salvo localmente editado em {}?"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Abrir o editor de rich text"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Título"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Subtítulo ou resumo"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Escreva seu artigo aqui. Markdown é suportado."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Cerca de {} caracteres restantes"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Tags"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licença"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Capa"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Isso é um rascunho"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Publicar"
+12 -12
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Romanian\n"
"Language: ro_RO\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Deschide editorul de text"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Titlu"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Scrie articolul tău aici. Markdown este acceptat."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "În apropiere de {} caractere rămase"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Etichete"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licenţă"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Coperta"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Aceasta este o ciornă"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Publică"
+16 -16
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "Хотите ли вы загрузить локальное автосохранение, сделанное в {}?"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
msgstr "Открыть в визуальном редакторе"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Заголовок"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
msgstr "Подзаголовок или резюме"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Пишите свою статью здесь. Markdown поддерживается."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Осталось около {} символов"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Теги"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
msgstr "Лицензия"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Обложка"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Это черновик"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Опубликовать"
+18 -18
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Santali\n"
"Language: sat_IN\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "ᱟᱢ ᱪᱮᱫ ᱢᱟᱲᱟᱝ {} ᱨᱮ ᱥᱟᱯᱲᱟᱣ ᱟᱠᱟᱱ ᱞᱚᱠᱟᱞ ᱚᱴᱚᱥᱮᱣ ᱞᱟᱫᱮ ᱥᱟᱱᱟᱢ ᱠᱟᱱᱟ ᱥᱮ?"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "ᱨᱤᱪ ᱚᱞ ᱥᱟᱯᱟᱣᱤᱡ ᱠᱷᱩᱞᱟᱹᱭ ᱢᱮ"
msgstr "ᱨᱤᱪ ᱚᱞ ᱥᱟᱯᱟᱣᱤᱡ ᱠᱷᱩᱞᱟᱹᱭ ᱢᱮ"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "ᱴᱭᱴᱚᱞ"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "ᱥᱟᱹᱵᱴᱟᱭᱴᱟᱹᱞ, ᱟᱨ ᱵᱟᱝ ᱥᱟᱹᱢᱢᱟᱨᱭ"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "ᱟᱢᱟᱜ ᱚᱱᱚᱞ ᱱᱚᱰᱮ ᱚᱞ ᱛᱟᱢ ᱾ ᱪᱤᱱᱦᱟᱹ ᱥᱟᱯᱚᱴ ᱜᱮᱭᱟ ᱾"
msgstr "ᱟᱢᱟᱜ ᱚᱱᱚᱞ ᱱᱚᱰᱮ ᱚᱞ ᱛᱟᱢ ᱾ ᱪᱤᱱᱦᱟᱹ ᱥᱟᱯᱚᱴ ᱜᱮᱭᱟ ᱾"
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "ᱡᱷᱚᱛᱚ ᱨᱮ {} ᱡᱤᱱᱤᱥ ᱵᱟᱧᱪᱟᱣᱠᱟᱱᱟ"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "ᱴᱮᱜᱥ"
msgstr "ᱴᱮᱜᱥ"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "ᱞᱚᱭᱥᱮᱱᱥ"
msgstr "ᱞᱚᱭᱥᱮᱱᱥ"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "ᱠᱚᱵᱷᱚᱨ"
msgstr "ᱢᱚᱭᱞᱟᱹᱴ"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "ᱱᱚᱶᱟ ᱫᱚ ᱰᱨᱟᱯᱷᱼᱴ ᱠᱟᱱᱟ"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "ᱯᱟᱨᱥᱟᱞ"
+14 -14
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Sinhala\n"
"Language: si_LK\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
msgstr "මාතෘකාව"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
msgstr "බලපත්‍රය"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""
+13 -13
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Slovak\n"
"Language: sk_SK\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "Chceš načítať automaticky uloženú zálohu, s poslednou úpravou {}?"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Otvor editor formátovaného textu"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Nadpis"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Zhrnutie, alebo podnadpis"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Tu napíš svoj článok. Markdown je podporovaný."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Zostáva asi {} znakov"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Štítky"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licencia"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Obálka"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Toto je koncept"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Zverejniť"
+12 -12
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Naslov"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Oznake"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licenca"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "To je osnutek"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Objavi"
+12 -12
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Serbian (Latin)\n"
"Language: sr_CS\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Otvori uređivač sa stilizacijom"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Naslov"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Podnaslov, ili sažetak"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Napišite vaš članak ovde. Na raspolaganju vam je Markdown."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Preostalo oko {} znakova"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Markeri"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licenca"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Naslovna strana"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Ovo je nacrt"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Objavi"
+12 -12
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Titel"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Skriv din artikel här. Markdown stöds."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Ungefär {} karaktärer kvar"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Taggar"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licens"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Omslag"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Publicera"
+12 -12
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Zengin metin editörünü (RTE) aç"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Başlık"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Alt başlık, veya açıklama"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Makaleni buraya yaz. Markdown kullanabilirsin."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Yaklaşık {} karakter kaldı"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Etiketler"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Lisans"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Kapak"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Bu bir taslaktır"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Yayınla"
+12 -12
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""
+63
View File
@@ -0,0 +1,63 @@
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: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Urdu (Pakistan)\n"
"Language: ur_PK\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-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: ur-PK\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""
+12 -12
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: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Vietnamese\n"
"Language: vi_VN\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Văn bản của tôi"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Tiêu Châu"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""
+30 -30
View File
@@ -3,61 +3,61 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
"Language-Team: Chinese Traditional\n"
"Language: zh_TW\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: zh-CN\n"
"X-Crowdin-Language: zh-TW\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr "打开富文本编辑器"
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "标题"
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr "副标题或摘要"
msgstr "你想要載入上次在 {} 编辑的本地自動保存嗎?"
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "開啟 RTF 編輯器"
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "標題"
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "副標題,或摘要"
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "在这里写下您的文章。支 Markdown 法。"
msgstr "在這裡寫下您的文章。支 Markdown 法。"
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "大约剩余 {} 可输入字符"
msgstr "大約還可輸入 {} 字符"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "标签"
msgstr "標籤"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "许可协议"
msgstr "授權條款"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "封面"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "这是一个草稿"
msgstr "這是草稿"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "布"
msgstr "布"
+133 -149
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: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:28\n"
"Last-Translator: \n"
"Language-Team: Afrikaans\n"
"Language: af_ZA\n"
@@ -18,106 +18,126 @@ msgstr ""
"X-Crowdin-File-ID: 8\n"
# src/template_utils.rs:105
msgid "{0} commented on your article."
msgstr ""
# src/template_utils.rs:106
msgid "{0} is subscribed to you."
msgid "Someone"
msgstr ""
# src/template_utils.rs:107
msgid "{0} liked your article."
msgid "{0} commented on your article."
msgstr ""
# src/template_utils.rs:108
msgid "{0} mentioned you."
msgid "{0} is subscribed to you."
msgstr ""
# src/template_utils.rs:109
msgid "{0} liked your article."
msgstr ""
# src/template_utils.rs:110
msgid "{0} mentioned you."
msgstr ""
# src/template_utils.rs:111
msgid "{0} boosted your article."
msgstr ""
# src/template_utils.rs:116
# src/template_utils.rs:118
msgid "Your feed"
msgstr ""
# src/template_utils.rs:117
# src/template_utils.rs:119
msgid "Local feed"
msgstr ""
# src/template_utils.rs:118
# src/template_utils.rs:120
msgid "Federated feed"
msgstr ""
# src/template_utils.rs:154
# src/template_utils.rs:156
msgid "{0}'s avatar"
msgstr ""
# src/template_utils.rs:198
# src/template_utils.rs:200
msgid "Previous page"
msgstr ""
# src/template_utils.rs:209
# src/template_utils.rs:211
msgid "Next page"
msgstr ""
# src/template_utils.rs:363
# src/template_utils.rs:365
msgid "Optional"
msgstr ""
# src/routes/blogs.rs:63
# src/routes/blogs.rs:68
msgid "To create a new blog, you need to be logged in"
msgstr ""
# src/routes/blogs.rs:102
# src/routes/blogs.rs:110
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/blogs.rs:140
# src/routes/blogs.rs:148
msgid "Your blog was successfully created!"
msgstr ""
# src/routes/blogs.rs:160
# src/routes/blogs.rs:166
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:168
# src/routes/blogs.rs:174
msgid "You are not allowed to delete this blog."
msgstr ""
# src/routes/blogs.rs:219
# src/routes/blogs.rs:224
msgid "You are not allowed to edit this blog."
msgstr ""
# src/routes/blogs.rs:275
# src/routes/blogs.rs:280
msgid "You can't use this media as a blog icon."
msgstr ""
# src/routes/blogs.rs:293
# src/routes/blogs.rs:298
msgid "You can't use this media as a blog banner."
msgstr ""
# src/routes/blogs.rs:327
# src/routes/blogs.rs:332
msgid "Your blog information have been updated."
msgstr ""
# src/routes/comments.rs:97
# src/routes/comments.rs:100
msgid "Your comment has been posted."
msgstr ""
# src/routes/comments.rs:172
# src/routes/comments.rs:177
msgid "Your comment has been deleted."
msgstr ""
# src/routes/instance.rs:120
# src/routes/email_signups.rs:82
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/email_signups.rs:119
msgid "User registration"
msgstr ""
# src/routes/email_signups.rs:120
msgid "Here is the link for registration: {0}"
msgstr ""
# src/routes/email_signups.rs:219
msgid "Your account has been created. Now you just need to log in, before you can use it."
msgstr ""
# src/routes/instance.rs:117
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:152
# src/routes/instance.rs:150
msgid "{} has been unblocked."
msgstr ""
# src/routes/instance.rs:154
# src/routes/instance.rs:152
msgid "{} has been blocked."
msgstr ""
@@ -125,170 +145,189 @@ msgstr ""
msgid "Blocks deleted"
msgstr ""
# src/routes/instance.rs:218
# src/routes/instance.rs:219
msgid "Email already blocked"
msgstr ""
# src/routes/instance.rs:223
# src/routes/instance.rs:224
msgid "Email Blocked"
msgstr ""
# src/routes/instance.rs:314
# src/routes/instance.rs:317
msgid "You can't change your own rights."
msgstr ""
# src/routes/instance.rs:325
# src/routes/instance.rs:328
msgid "You are not allowed to take this action."
msgstr ""
# src/routes/instance.rs:362
# src/routes/instance.rs:363
msgid "Done."
msgstr ""
# src/routes/likes.rs:53
# src/routes/likes.rs:58
msgid "To like a post, you need to be logged in"
msgstr ""
# src/routes/medias.rs:145
# src/routes/medias.rs:153
msgid "Your media have been deleted."
msgstr ""
# src/routes/medias.rs:150
# src/routes/medias.rs:158
msgid "You are not allowed to delete this media."
msgstr ""
# src/routes/medias.rs:167
# src/routes/medias.rs:175
msgid "Your avatar has been updated."
msgstr ""
# src/routes/medias.rs:172
# src/routes/medias.rs:180
msgid "You are not allowed to use this media."
msgstr ""
# src/routes/notifications.rs:28
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr ""
# src/routes/posts.rs:54
# src/routes/posts.rs:56
msgid "This post isn't published yet."
msgstr ""
# src/routes/posts.rs:125
# src/routes/posts.rs:126
msgid "To write a new post, you need to be logged in"
msgstr ""
# src/routes/posts.rs:142
# src/routes/posts.rs:147
msgid "You are not an author of this blog."
msgstr ""
# src/routes/posts.rs:149
# src/routes/posts.rs:154
msgid "New post"
msgstr ""
# src/routes/posts.rs:194
# src/routes/posts.rs:199
msgid "Edit {0}"
msgstr ""
# src/routes/posts.rs:263
# src/routes/posts.rs:268
msgid "You are not allowed to publish on this blog."
msgstr ""
# src/routes/posts.rs:355
# src/routes/posts.rs:368
msgid "Your article has been updated."
msgstr ""
# src/routes/posts.rs:542
# src/routes/posts.rs:557
msgid "Your article has been saved."
msgstr ""
# src/routes/posts.rs:549
# src/routes/posts.rs:564
msgid "New article"
msgstr ""
# src/routes/posts.rs:582
# src/routes/posts.rs:602
msgid "You are not allowed to delete this article."
msgstr ""
# src/routes/posts.rs:607
# src/routes/posts.rs:626
msgid "Your article has been deleted."
msgstr ""
# src/routes/posts.rs:612
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
# src/routes/posts.rs:631
msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?"
msgstr ""
# src/routes/posts.rs:652
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
"username is correct."
# src/routes/posts.rs:673
msgid "Couldn't obtain enough information about your account. Please make sure your username is correct."
msgstr ""
# src/routes/reshares.rs:54
# src/routes/reshares.rs:58
msgid "To reshare a post, you need to be logged in"
msgstr ""
# src/routes/session.rs:87
# src/routes/session.rs:95
msgid "You are now connected."
msgstr ""
# src/routes/session.rs:108
# src/routes/session.rs:116
msgid "You are now logged off."
msgstr ""
# src/routes/session.rs:153
# src/routes/session.rs:162
msgid "Password reset"
msgstr ""
# src/routes/session.rs:154
# src/routes/session.rs:163
msgid "Here is the link to reset your password: {0}"
msgstr ""
# src/routes/session.rs:216
# src/routes/session.rs:235
msgid "Your password was successfully reset."
msgstr ""
# src/routes/user.rs:141
# src/routes/user.rs:87
msgid "To access your dashboard, you need to be logged in"
msgstr ""
# src/routes/user.rs:163
# src/routes/user.rs:109
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:180
# src/routes/user.rs:126
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:260
# src/routes/user.rs:203
msgid "To subscribe to someone, you need to be logged in"
msgstr ""
# src/routes/user.rs:364
# src/routes/user.rs:323
msgid "To edit your profile, you need to be logged in"
msgstr ""
# src/routes/user.rs:409
# src/routes/user.rs:369
msgid "Your profile has been updated."
msgstr ""
# src/routes/user.rs:436
# src/routes/user.rs:397
msgid "Your account has been deleted."
msgstr ""
# src/routes/user.rs:442
# src/routes/user.rs:403
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:526
msgid "Registrations are closed on this instance."
msgid "Create your account"
msgstr ""
# src/routes/user.rs:549
msgid ""
"Your account has been created. Now you just need to log in, before you can "
"use it."
msgid "Create an account"
msgstr ""
msgid "Email"
msgstr ""
msgid "Email confirmation"
msgstr ""
msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one."
msgstr ""
msgid "Registration"
msgstr ""
msgid "Check your inbox!"
msgstr ""
msgid "We sent a mail to the address you gave us, with a link for registration."
msgstr ""
msgid "Username"
msgstr ""
msgid "Password"
msgstr ""
msgid "Password confirmation"
msgstr ""
msgid "Media upload"
@@ -429,29 +468,6 @@ msgstr ""
msgid "Subscriptions"
msgstr ""
msgid "Create your account"
msgstr ""
msgid "Create an account"
msgstr ""
msgid "Username"
msgstr ""
msgid "Email"
msgstr ""
msgid "Password"
msgstr ""
msgid "Password confirmation"
msgstr ""
msgid ""
"Apologies, but registrations are closed on this particular instance. You "
"can, however, find a different one."
msgstr ""
msgid "{0}'s subscriptions"
msgstr ""
@@ -479,8 +495,7 @@ msgstr ""
msgid "Your Profile"
msgstr ""
msgid ""
"To change your avatar, upload it to your gallery and then select from there."
msgid "To change your avatar, upload it to your gallery and then select from there."
msgstr ""
msgid "Upload an avatar"
@@ -552,10 +567,7 @@ msgstr ""
msgid "Invalid CSRF token"
msgstr ""
msgid ""
"Something is wrong with your CSRF token. Make sure cookies are enabled in "
"you browser, and try reloading this page. If you continue to see this error "
"message, please report it."
msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it."
msgstr ""
msgid "You are not authorized."
@@ -639,21 +651,13 @@ msgstr ""
msgid "Save these settings"
msgstr ""
msgid ""
"If you are browsing this site as a visitor, no data about you is collected."
msgid "If you are browsing this site as a visitor, no data about you is collected."
msgstr ""
msgid ""
"As a registered user, you have to provide your username (which does not have "
"to be your real name), your functional email address and a password, in "
"order to be able to log in, write articles and comment. The content you "
"submit is stored until you delete it."
msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it."
msgstr ""
msgid ""
"When you log in, we store two cookies, one to keep your session open, the "
"second to prevent other people to act on your behalf. We don't store any "
"other cookies."
msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies."
msgstr ""
msgid "Blocklisted Emails"
@@ -662,10 +666,7 @@ msgstr ""
msgid "Email address"
msgstr ""
msgid ""
"The email address you wish to block. In order to block domains, you can use "
"globbing syntax, for example '*@example.com' blocks all addresses from "
"example.com"
msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com"
msgstr ""
msgid "Note"
@@ -674,17 +675,13 @@ msgstr ""
msgid "Notify the user?"
msgstr ""
msgid ""
"Optional, shows a message to the user when they attempt to create an account "
"with that address"
msgid "Optional, shows a message to the user when they attempt to create an account with that address"
msgstr ""
msgid "Blocklisting notification"
msgstr ""
msgid ""
"The message to be shown when the user attempts to create an account with "
"this email address"
msgid "The message to be shown when the user attempts to create an account with this email address"
msgstr ""
msgid "Add blocklisted address"
@@ -756,9 +753,7 @@ msgstr ""
msgid "Content"
msgstr ""
msgid ""
"You can upload media to your gallery, and then copy their Markdown code into "
"your articles to insert them."
msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them."
msgstr ""
msgid "Upload media"
@@ -816,9 +811,7 @@ msgstr ""
msgid "Boost"
msgstr ""
msgid ""
"{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this "
"article"
msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article"
msgstr ""
msgid "Comments"
@@ -872,12 +865,7 @@ msgstr ""
msgid "Update password"
msgstr ""
msgid "Check your inbox!"
msgstr ""
msgid ""
"We sent a mail to the address you gave us, with a link to reset your "
"password."
msgid "We sent a mail to the address you gave us, with a link to reset your password."
msgstr ""
msgid "Send password reset link"
@@ -886,9 +874,7 @@ msgstr ""
msgid "This token has expired"
msgstr ""
msgid ""
"Please start the process again by clicking <a href=\"/password-reset\">here</"
"a>."
msgid "Please start the process again by clicking <a href=\"/password-reset\">here</a>."
msgstr ""
msgid "New Blog"
@@ -903,8 +889,7 @@ msgstr ""
msgid "Edit \"{}\""
msgstr ""
msgid ""
"You can upload images to your gallery, to use them as blog icons, or banners."
msgid "You can upload images to your gallery, to use them as blog icons, or banners."
msgstr ""
msgid "Upload images"
@@ -966,9 +951,7 @@ msgstr ""
msgid "Authors can manage multiple blogs, each as its own website."
msgstr ""
msgid ""
"Articles are also visible on other Plume instances, and you can interact "
"with them directly from other platforms like Mastodon."
msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon."
msgstr ""
msgid "Read the detailed rules"
@@ -1048,3 +1031,4 @@ msgstr ""
msgid "Article license"
msgstr ""
+139 -167
View File
@@ -3,15 +3,14 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:28\n"
"Last-Translator: \n"
"Language-Team: Arabic\n"
"Language: ar_SA\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: ar\n"
@@ -19,106 +18,126 @@ msgstr ""
"X-Crowdin-File-ID: 8\n"
# src/template_utils.rs:105
msgid "Someone"
msgstr ""
# src/template_utils.rs:107
msgid "{0} commented on your article."
msgstr "علّق {0} على مقالك."
# src/template_utils.rs:106
# src/template_utils.rs:108
msgid "{0} is subscribed to you."
msgstr "{0} مشترك لك."
# src/template_utils.rs:107
# src/template_utils.rs:109
msgid "{0} liked your article."
msgstr "{0} أعجبهم مقالك."
# src/template_utils.rs:108
# src/template_utils.rs:110
msgid "{0} mentioned you."
msgstr "أشار إليك {0}."
# src/template_utils.rs:109
# src/template_utils.rs:111
msgid "{0} boosted your article."
msgstr "{0} دعمو مقالك."
# src/template_utils.rs:116
# src/template_utils.rs:118
msgid "Your feed"
msgstr "خيطك"
# src/template_utils.rs:117
# src/template_utils.rs:119
msgid "Local feed"
msgstr "الخيط المحلي"
# src/template_utils.rs:118
# src/template_utils.rs:120
msgid "Federated feed"
msgstr "الخيط الموحد"
# src/template_utils.rs:154
# src/template_utils.rs:156
msgid "{0}'s avatar"
msgstr "الصورة الرمزية لـ {0}"
# src/template_utils.rs:198
# src/template_utils.rs:200
msgid "Previous page"
msgstr "الصفحة السابقة"
# src/template_utils.rs:209
# src/template_utils.rs:211
msgid "Next page"
msgstr "الصفحة التالية"
# src/template_utils.rs:363
# src/template_utils.rs:365
msgid "Optional"
msgstr "اختياري"
# src/routes/blogs.rs:63
# src/routes/blogs.rs:68
msgid "To create a new blog, you need to be logged in"
msgstr "لإنشاء مدونة جديدة، تحتاج إلى تسجيل الدخول"
# src/routes/blogs.rs:102
# src/routes/blogs.rs:110
msgid "A blog with the same name already exists."
msgstr "توجد مدونة تحمل نفس العنوان."
# src/routes/blogs.rs:140
# src/routes/blogs.rs:148
msgid "Your blog was successfully created!"
msgstr "تم إنشاء مدونتك بنجاح!"
# src/routes/blogs.rs:160
# src/routes/blogs.rs:166
msgid "Your blog was deleted."
msgstr "تم حذف مدونتك."
# src/routes/blogs.rs:168
# src/routes/blogs.rs:174
msgid "You are not allowed to delete this blog."
msgstr "لا يسمح لك بحذف هذه المدونة."
# src/routes/blogs.rs:219
# src/routes/blogs.rs:224
msgid "You are not allowed to edit this blog."
msgstr "لا يسمح لك بتعديل هذه المدونة."
# src/routes/blogs.rs:275
# src/routes/blogs.rs:280
msgid "You can't use this media as a blog icon."
msgstr "لا يمكنك استخدام هذه الوسائط كأيقونة للمدونة."
# src/routes/blogs.rs:293
# src/routes/blogs.rs:298
msgid "You can't use this media as a blog banner."
msgstr "لا يمكنك استخدام هذه الوسائط كشعار للمدونة."
# src/routes/blogs.rs:327
# src/routes/blogs.rs:332
msgid "Your blog information have been updated."
msgstr "تم تحديث معلومات مُدوّنتك."
# src/routes/comments.rs:97
# src/routes/comments.rs:100
msgid "Your comment has been posted."
msgstr "تم نشر تعليقك."
# src/routes/comments.rs:172
# src/routes/comments.rs:177
msgid "Your comment has been deleted."
msgstr "تم حذف تعليقك."
# src/routes/instance.rs:120
# src/routes/email_signups.rs:82
msgid "Registrations are closed on this instance."
msgstr "التسجيلات مُغلقة على مثيل الخادم هذ."
# src/routes/email_signups.rs:119
msgid "User registration"
msgstr ""
# src/routes/email_signups.rs:120
msgid "Here is the link for registration: {0}"
msgstr ""
# src/routes/email_signups.rs:219
msgid "Your account has been created. Now you just need to log in, before you can use it."
msgstr "لقد تم إنشاء حسابك. ما عليك إلّا الولوج الآن للتمكّن مِن استعماله."
# src/routes/instance.rs:117
msgid "Instance settings have been saved."
msgstr "تم حفظ إعدادات المثيل."
# src/routes/instance.rs:152
# src/routes/instance.rs:150
msgid "{} has been unblocked."
msgstr "تم إلغاء حظر {}."
# src/routes/instance.rs:154
# src/routes/instance.rs:152
msgid "{} has been blocked."
msgstr "تم حظر {}."
@@ -126,171 +145,190 @@ msgstr "تم حظر {}."
msgid "Blocks deleted"
msgstr ""
# src/routes/instance.rs:218
# src/routes/instance.rs:219
msgid "Email already blocked"
msgstr ""
# src/routes/instance.rs:223
# src/routes/instance.rs:224
msgid "Email Blocked"
msgstr "البريد الإلكتروني محظور"
# src/routes/instance.rs:314
# src/routes/instance.rs:317
msgid "You can't change your own rights."
msgstr ""
# src/routes/instance.rs:325
# src/routes/instance.rs:328
msgid "You are not allowed to take this action."
msgstr "لا يسمح لك القيام بهذا الإجراء."
# src/routes/instance.rs:362
# src/routes/instance.rs:363
msgid "Done."
msgstr "تم."
# src/routes/likes.rs:53
# src/routes/likes.rs:58
msgid "To like a post, you need to be logged in"
msgstr "يجب عليك تسجيل الدخول أولا للإعجاب بهذا المقال"
# src/routes/medias.rs:145
# src/routes/medias.rs:153
msgid "Your media have been deleted."
msgstr "لقد تم حذف وسائطك."
# src/routes/medias.rs:150
# src/routes/medias.rs:158
msgid "You are not allowed to delete this media."
msgstr "لا يسمح لك بحذف هذه الوسائط."
# src/routes/medias.rs:167
# src/routes/medias.rs:175
msgid "Your avatar has been updated."
msgstr "تم تحديث صورتك الشخصية."
# src/routes/medias.rs:172
# src/routes/medias.rs:180
msgid "You are not allowed to use this media."
msgstr "لا يسمح لك باستعمال هذه الوسائط."
# src/routes/notifications.rs:28
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr "يجب عليك تسجيل الدخول أولا لعرض الإشعارات"
# src/routes/posts.rs:54
# src/routes/posts.rs:56
msgid "This post isn't published yet."
msgstr "هذا المقال ليس منشورا بعد."
# src/routes/posts.rs:125
# src/routes/posts.rs:126
msgid "To write a new post, you need to be logged in"
msgstr "يجب عليك تسجيل الدخول أولا لكتابة مقال جديد"
# src/routes/posts.rs:142
# src/routes/posts.rs:147
msgid "You are not an author of this blog."
msgstr "لست مِن محرري هذه المدونة."
# src/routes/posts.rs:149
# src/routes/posts.rs:154
msgid "New post"
msgstr "منشور جديد"
# src/routes/posts.rs:194
# src/routes/posts.rs:199
msgid "Edit {0}"
msgstr "تعديل {0}"
# src/routes/posts.rs:263
# src/routes/posts.rs:268
msgid "You are not allowed to publish on this blog."
msgstr "لا يسمح لك بالنشر على هذه المدونة."
# src/routes/posts.rs:355
# src/routes/posts.rs:368
msgid "Your article has been updated."
msgstr "تم تحديث مقالك."
# src/routes/posts.rs:542
# src/routes/posts.rs:557
msgid "Your article has been saved."
msgstr "تم حفظ مقالك."
# src/routes/posts.rs:549
# src/routes/posts.rs:564
msgid "New article"
msgstr "مقال جديد"
# src/routes/posts.rs:582
# src/routes/posts.rs:602
msgid "You are not allowed to delete this article."
msgstr "لا يسمح لك بحذف هذا المقال."
# src/routes/posts.rs:607
# src/routes/posts.rs:626
msgid "Your article has been deleted."
msgstr "تم حذف مقالك."
# src/routes/posts.rs:612
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
# src/routes/posts.rs:631
msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?"
msgstr "لم يتم العثور على المقال الذي تحاول حذفه. ربما سبق حذفه؟"
# src/routes/posts.rs:652
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
"username is correct."
# src/routes/posts.rs:673
msgid "Couldn't obtain enough information about your account. Please make sure your username is correct."
msgstr "تعذر العثور عن معلومات حسابك. المرجو التحقق من صحة إسم المستخدم."
# src/routes/reshares.rs:54
# src/routes/reshares.rs:58
msgid "To reshare a post, you need to be logged in"
msgstr "يجب عليك تسجيل الدخول أولا للإعادت نشر هذا المقال"
# src/routes/session.rs:87
# src/routes/session.rs:95
msgid "You are now connected."
msgstr "أنت الآن متصل."
# src/routes/session.rs:108
# src/routes/session.rs:116
msgid "You are now logged off."
msgstr "لقد قمتَ بالخروج للتوّ."
# src/routes/session.rs:153
# src/routes/session.rs:162
msgid "Password reset"
msgstr "إعادة تعيين كلمة المرور"
# src/routes/session.rs:154
# src/routes/session.rs:163
msgid "Here is the link to reset your password: {0}"
msgstr "ها هو رابط إعادة تعيين كلمتك السرية: {0}"
# src/routes/session.rs:216
# src/routes/session.rs:235
msgid "Your password was successfully reset."
msgstr "تمت إعادة تعيين كلمتك السرية بنجاح."
# src/routes/user.rs:141
# src/routes/user.rs:87
msgid "To access your dashboard, you need to be logged in"
msgstr "يجب عليك تسجيل الدخول أولاللنفاذ إلى لوح المراقبة"
# src/routes/user.rs:163
# src/routes/user.rs:109
msgid "You are no longer following {}."
msgstr "أنت لم تعد تتابع {}."
# src/routes/user.rs:180
# src/routes/user.rs:126
msgid "You are now following {}."
msgstr "أنت الآن تتابع {}."
# src/routes/user.rs:260
# src/routes/user.rs:203
msgid "To subscribe to someone, you need to be logged in"
msgstr "للإشتراك بأحد ما، يجب تسجيل الدخول أولا"
# src/routes/user.rs:364
# src/routes/user.rs:323
msgid "To edit your profile, you need to be logged in"
msgstr "لتعديل الحساب، يجب تسجيل الدخول أولا"
# src/routes/user.rs:409
# src/routes/user.rs:369
msgid "Your profile has been updated."
msgstr "تم تحديث ملفك الشخصي."
# src/routes/user.rs:436
# src/routes/user.rs:397
msgid "Your account has been deleted."
msgstr "تم حذف حسابك."
# src/routes/user.rs:442
# src/routes/user.rs:403
msgid "You can't delete someone else's account."
msgstr "لا يمكنك حذف حساب شخص آخر."
# src/routes/user.rs:526
msgid "Registrations are closed on this instance."
msgstr "التسجيلات مُغلقة على مثيل الخادم هذ."
msgid "Create your account"
msgstr "انشئ حسابك"
# src/routes/user.rs:549
msgid ""
"Your account has been created. Now you just need to log in, before you can "
"use it."
msgstr "لقد تم إنشاء حسابك. ما عليك إلّا الولوج الآن للتمكّن مِن استعماله."
msgid "Create an account"
msgstr "انشئ حسابا"
msgid "Email"
msgstr "البريد الالكتروني"
msgid "Email confirmation"
msgstr ""
msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one."
msgstr "المعذرة، لاكن التسجيل مغلق في هذا المثيل بالدات. يمكنك إجاد مثيل آخر للتسجيل."
msgid "Registration"
msgstr ""
msgid "Check your inbox!"
msgstr "تحقق من علبة الوارد الخاصة بك!"
msgid "We sent a mail to the address you gave us, with a link for registration."
msgstr ""
msgid "Username"
msgstr "اسم المستخدم"
msgid "Password"
msgstr "كلمة السر"
msgid "Password confirmation"
msgstr ""
msgid "Media upload"
msgstr "إرسال الوسائط"
@@ -430,30 +468,6 @@ msgstr "المشترِكون"
msgid "Subscriptions"
msgstr "الاشتراكات"
msgid "Create your account"
msgstr "انشئ حسابك"
msgid "Create an account"
msgstr "انشئ حسابا"
msgid "Username"
msgstr "اسم المستخدم"
msgid "Email"
msgstr "البريد الالكتروني"
msgid "Password"
msgstr "كلمة السر"
msgid "Password confirmation"
msgstr ""
msgid ""
"Apologies, but registrations are closed on this particular instance. You "
"can, however, find a different one."
msgstr ""
"المعذرة، لاكن التسجيل مغلق في هذا المثيل بالدات. يمكنك إجاد مثيل آخر للتسجيل."
msgid "{0}'s subscriptions"
msgstr "{0} اشتراكات"
@@ -481,10 +495,8 @@ msgstr "تعديل حسابك"
msgid "Your Profile"
msgstr "ملفك الشخصي"
msgid ""
"To change your avatar, upload it to your gallery and then select from there."
msgstr ""
"لتغير الصورة التشخيصية قم أولا برفعها إلى الألبوم ثم قم بتعينها من هنالك."
msgid "To change your avatar, upload it to your gallery and then select from there."
msgstr "لتغير الصورة التشخيصية قم أولا برفعها إلى الألبوم ثم قم بتعينها من هنالك."
msgid "Upload an avatar"
msgstr "تحميل صورة رمزية"
@@ -555,13 +567,8 @@ msgstr "نعتذر عن الإزعاج. إن كنت تضن أن هذه مشكل
msgid "Invalid CSRF token"
msgstr "الرمز المميز CSRF غير صالح"
msgid ""
"Something is wrong with your CSRF token. Make sure cookies are enabled in "
"you browser, and try reloading this page. If you continue to see this error "
"message, please report it."
msgstr ""
"هناكخطأم ما في رمز CSRF. تحقق أن الكوكيز مفعل في متصفحك وأعد تحميل الصفحة. "
"إذا واجهتهذا الخطأ منجديد يرجى التبليغ."
msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it."
msgstr "هناكخطأم ما في رمز CSRF. تحقق أن الكوكيز مفعل في متصفحك وأعد تحميل الصفحة. إذا واجهتهذا الخطأ منجديد يرجى التبليغ."
msgid "You are not authorized."
msgstr "ليست لديك التصريحات اللازمة للقيام بذلك."
@@ -644,21 +651,13 @@ msgstr "الرخصة الافتراضية للمقال"
msgid "Save these settings"
msgstr "احفظ هذه الإعدادات"
msgid ""
"If you are browsing this site as a visitor, no data about you is collected."
msgid "If you are browsing this site as a visitor, no data about you is collected."
msgstr "إذا كنت تصفح هذا الموقع كزائر ، لا يتم تجميع أي بيانات عنك."
msgid ""
"As a registered user, you have to provide your username (which does not have "
"to be your real name), your functional email address and a password, in "
"order to be able to log in, write articles and comment. The content you "
"submit is stored until you delete it."
msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it."
msgstr ""
msgid ""
"When you log in, we store two cookies, one to keep your session open, the "
"second to prevent other people to act on your behalf. We don't store any "
"other cookies."
msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies."
msgstr ""
msgid "Blocklisted Emails"
@@ -667,10 +666,7 @@ msgstr ""
msgid "Email address"
msgstr ""
msgid ""
"The email address you wish to block. In order to block domains, you can use "
"globbing syntax, for example '*@example.com' blocks all addresses from "
"example.com"
msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com"
msgstr ""
msgid "Note"
@@ -679,17 +675,13 @@ msgstr ""
msgid "Notify the user?"
msgstr ""
msgid ""
"Optional, shows a message to the user when they attempt to create an account "
"with that address"
msgid "Optional, shows a message to the user when they attempt to create an account with that address"
msgstr ""
msgid "Blocklisting notification"
msgstr ""
msgid ""
"The message to be shown when the user attempts to create an account with "
"this email address"
msgid "The message to be shown when the user attempts to create an account with this email address"
msgstr ""
msgid "Add blocklisted address"
@@ -761,11 +753,8 @@ msgstr "العنوان الثانوي"
msgid "Content"
msgstr "المحتوى"
msgid ""
"You can upload media to your gallery, and then copy their Markdown code into "
"your articles to insert them."
msgstr ""
"يكنك رفع الوسائط للألبوم ومن ثم نسخ شفرة الماركداون في مقالاتك لإدراجها."
msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them."
msgstr "يكنك رفع الوسائط للألبوم ومن ثم نسخ شفرة الماركداون في مقالاتك لإدراجها."
msgid "Upload media"
msgstr "تحميل وسائط"
@@ -830,12 +819,8 @@ msgstr "لم أعد أرغب في دعم هذا"
msgid "Boost"
msgstr "رقّي"
msgid ""
"{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this "
"article"
msgstr ""
"{0}قم بتسجيل الدخول{1} أو {2}استخدم حسابك على الفديفرس{3} إن كنت ترغب في "
"التفاعل مع هذا المقال"
msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article"
msgstr "{0}قم بتسجيل الدخول{1} أو {2}استخدم حسابك على الفديفرس{3} إن كنت ترغب في التفاعل مع هذا المقال"
msgid "Comments"
msgstr "التعليقات"
@@ -888,15 +873,8 @@ msgstr "تأكيد"
msgid "Update password"
msgstr "تحديث الكلمة السرية"
msgid "Check your inbox!"
msgstr "تحقق من علبة الوارد الخاصة بك!"
msgid ""
"We sent a mail to the address you gave us, with a link to reset your "
"password."
msgstr ""
"لقد أرسلنا رسالة للعنوان الذي توصلنا به من طرفك تضمنرابط لإعادت تحديد كلمة "
"المرور."
msgid "We sent a mail to the address you gave us, with a link to reset your password."
msgstr "لقد أرسلنا رسالة للعنوان الذي توصلنا به من طرفك تضمنرابط لإعادت تحديد كلمة المرور."
msgid "Send password reset link"
msgstr "أرسل رابط إعادة تعيين الكلمة السرية"
@@ -904,9 +882,7 @@ msgstr "أرسل رابط إعادة تعيين الكلمة السرية"
msgid "This token has expired"
msgstr ""
msgid ""
"Please start the process again by clicking <a href=\"/password-reset\">here</"
"a>."
msgid "Please start the process again by clicking <a href=\"/password-reset\">here</a>."
msgstr ""
msgid "New Blog"
@@ -921,8 +897,7 @@ msgstr "انشاء مدونة"
msgid "Edit \"{}\""
msgstr "تعديل \"{}\""
msgid ""
"You can upload images to your gallery, to use them as blog icons, or banners."
msgid "You can upload images to your gallery, to use them as blog icons, or banners."
msgstr "يمكن رفع الصور إلى ألبومك من أجل إستعمالها كأيقونة المدونة أو الشعار."
msgid "Upload images"
@@ -988,12 +963,8 @@ msgstr "بلوم محرك لامركزي للمدونات."
msgid "Authors can manage multiple blogs, each as its own website."
msgstr "يمكن للمحررين أن يديرو العديد من المدونات كل واحدة كموقع منفرد."
msgid ""
"Articles are also visible on other Plume instances, and you can interact "
"with them directly from other platforms like Mastodon."
msgstr ""
"ستكون المقالات معروضة على مواقع بلومالأخرى حيث يمكنكم التفاعل معها مباشرة "
"عبر أية منصة أخرى مثل ماستدون."
msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon."
msgstr "ستكون المقالات معروضة على مواقع بلومالأخرى حيث يمكنكم التفاعل معها مباشرة عبر أية منصة أخرى مثل ماستدون."
msgid "Read the detailed rules"
msgstr "إقرأ القواعد بالتفصيل"
@@ -1072,3 +1043,4 @@ msgstr "نشرتحت هذا الترخيص"
msgid "Article license"
msgstr "رخصة المقال"

Some files were not shown because too many files have changed in this diff Show More