Compare commits

..

185 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
173 changed files with 10257 additions and 9698 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
@@ -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 }}
+30 -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,15 +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
@@ -30,6 +53,9 @@
- 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
@@ -210,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
+608 -533
View File
File diff suppressed because it is too large Load Diff
+10 -14
View File
@@ -1,36 +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"
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 = "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]]
@@ -52,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"
@@ -69,7 +65,7 @@ rev = "29910f2829e7e590a540da3804336577b48c7b31"
[build-dependencies]
ructe = "0.13.0"
rsass = "0.9"
rsass = "0.23"
[features]
default = ["postgres"]
+7
View File
@@ -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(())
@@ -1,2 +0,0 @@
ALTER TABLE instances DROP COLUMN private_key;
ALTER TABLE instances DROP COLUMN public_key;
@@ -1,2 +0,0 @@
ALTER TABLE instances ADD COLUMN private_key TEXT;
ALTER TABLE instances ADD COLUMN public_key TEXT;
@@ -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);
@@ -1,30 +0,0 @@
CREATE TABLE instances_old (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
public_domain VARCHAR NOT NULL,
name VARCHAR NOT NULL,
local BOOLEAN NOT NULL DEFAULT 'f',
blocked BOOLEAN NOT NULL DEFAULT 'f',
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
open_registrations BOOLEAN NOT NULL DEFAULT 't',
short_description TEXT NOT NULL DEFAULT '',
long_description TEXT NOT NULL DEFAULT '',
default_license TEXT NOT NULL DEFAULT 'CC-0',
long_description_html VARCHAR NOT NULL DEFAULT '',
short_description_html VARCHAR NOT NULL DEFAULT ''
);
INSERT INTO instances_old SELECT
id,
public_domain,
name,
local,
blocked,
creation_date,
open_registrations,
short_description,
long_description,
default_license,
long_description_html,
short_description_html
FROM instances;
DROP TABLE instances;
ALTER TABLE instances_old RENAME TO instances;
@@ -1,2 +0,0 @@
ALTER TABLE instances ADD COLUMN private_key TEXT;
ALTER TABLE instances ADD COLUMN public_key TEXT;
@@ -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 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "plume-cli"
version = "0.6.1-dev"
version = "0.7.1"
authors = ["Plume contributors"]
edition = "2018"
@@ -10,7 +10,7 @@ path = "src/main.rs"
[dependencies]
clap = "2.33"
dotenv = "0.14"
dotenv = "0.15"
rpassword = "5.0.0"
[dependencies.diesel]
+18 -2
View File
@@ -1,6 +1,6 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use plume_models::{instance::NewInstance, Connection};
use plume_models::{instance::*, safe_string::SafeString, Connection};
use std::env;
pub fn command<'a, 'b>() -> App<'a, 'b> {
@@ -53,5 +53,21 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
.unwrap_or_else(|| String::from("CC-BY-SA"));
let open_reg = !args.is_present("private");
NewInstance::new_local(conn, domain, name, open_reg, license).expect("Couldn't save instance");
Instance::insert(
conn,
NewInstance {
public_domain: domain,
name,
local: true,
long_description: SafeString::new(""),
short_description: SafeString::new(""),
default_license: license,
open_registrations: open_reg,
short_description_html: String::new(),
long_description_html: String::new(),
},
)
.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)) => {
+12 -7
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"
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]
+204 -170
View File
@@ -1,10 +1,7 @@
use reqwest;
use std::fmt::Debug;
use super::{request, sign::Signer};
use reqwest::{
header::{HeaderValue, HOST},
Url,
};
/// Represents an ActivityPub inbox.
///
@@ -16,55 +13,12 @@ use reqwest::{
/// # extern crate activitypub;
/// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note};
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
/// # use plume_common::activity_pub::{inbox::*, sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer}};
/// # struct User;
/// # impl FromId<()> for User {
/// # type Error = ();
/// # type Object = Person;
/// # 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};
/// #
/// # fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
/// # Ok(User)
/// # }
/// # static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
/// #
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
/// # Ok(User)
/// # }
/// # }
/// # impl AsActor<&()> for User {
/// # fn get_inbox_url(&self) -> String {
/// # String::new()
/// # }
/// # fn is_local(&self) -> bool { false }
/// # }
/// # struct Message;
/// # impl FromId<()> for Message {
/// # type Error = ();
/// # type Object = Note;
/// #
/// # fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
/// # Ok(Message)
/// # }
/// #
/// # fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
/// # Ok(Message)
/// # }
/// # }
/// # impl AsObject<User, Create, &()> for Message {
/// # type Error = ();
/// # type Output = ();
/// #
/// # fn activity(self, _: &(), _actor: User, _id: &str) -> Result<(), ()> {
/// # Ok(())
/// # }
/// # }
/// # impl AsObject<User, Announce, &()> for Message {
/// # type Error = ();
/// # type Output = ();
/// #
/// # fn activity(self, _: &(), _actor: User, _id: &str) -> Result<(), ()> {
/// # Ok(())
/// # }
/// # }
/// # struct MySigner {
/// # public_key: String,
/// # private_key: String,
@@ -85,20 +39,77 @@ use reqwest::{
/// # "mysigner".into()
/// # }
/// #
/// # fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
/// # 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(|_| SignatureError())
/// # signer.sign_to_vec().map_err(|_| SignError())
/// # }
/// #
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<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()).unwrap())
/// # .unwrap();
/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
/// # verifier.update(data.as_bytes()).unwrap();
/// # verifier.verify(&signature).map_err(|_| SignatureError())
/// # verifier.verify(&signature).map_err(|_| SignError())
/// # }
/// # }
/// #
/// # struct User;
/// # impl FromId<()> for User {
/// # type Error = ();
/// # type Object = Person;
/// #
/// # fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
/// # Ok(User)
/// # }
/// #
/// # 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 {
/// # String::new()
/// # }
/// # fn is_local(&self) -> bool { false }
/// # }
/// # struct Message;
/// # impl FromId<()> for Message {
/// # type Error = ();
/// # type Object = Note;
/// #
/// # fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
/// # Ok(Message)
/// # }
/// #
/// # 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 = ();
/// # type Output = ();
/// #
/// # fn activity(self, _: &(), _actor: User, _id: &str) -> Result<(), ()> {
/// # Ok(())
/// # }
/// # }
/// # impl AsObject<User, Announce, &()> for Message {
/// # type Error = ();
/// # type Output = ();
/// #
/// # fn activity(self, _: &(), _actor: User, _id: &str) -> Result<(), ()> {
/// # Ok(())
/// # }
/// # }
/// #
@@ -111,9 +122,8 @@ use reqwest::{
/// # let activity_json = serde_json::to_value(act).unwrap();
/// #
/// # let conn = ();
/// # let sender = MySigner::new();
/// #
/// let result: Result<(), ()> = Inbox::handle(&conn, &sender, activity_json)
/// let result: Result<(), ()> = Inbox::handle(&conn, activity_json)
/// .with::<User, Announce, Message>(None)
/// .with::<User, Create, Message>(None)
/// .done();
@@ -127,10 +137,9 @@ where
/// # Structure
///
/// - the context to be passed to each handler.
/// - the sender actor to sign request
/// - the activity
/// - the reason it has not been handled yet
NotHandled(&'a C, &'a dyn Signer, serde_json::Value, InboxError<E>),
NotHandled(&'a C, serde_json::Value, InboxError<E>),
/// A matching handler have been found but failed
///
@@ -183,12 +192,8 @@ where
///
/// - `ctx`: the context to pass to each handler
/// - `json`: the JSON representation of the incoming activity
pub fn handle(
ctx: &'a C,
sender: &'a dyn Signer,
json: serde_json::Value,
) -> Inbox<'a, C, E, R> {
Inbox::NotHandled(ctx, sender, json, InboxError::NoMatch)
pub fn handle(ctx: &'a C, json: serde_json::Value) -> Inbox<'a, C, E, R> {
Inbox::NotHandled(ctx, json, InboxError::NoMatch)
}
/// Registers an handler on this Inbox.
@@ -199,30 +204,27 @@ where
M: AsObject<A, V, &'a C, Error = E> + FromId<C, Error = E>,
M::Output: Into<R>,
{
if let Inbox::NotHandled(ctx, sender, mut act, e) = self {
if let Inbox::NotHandled(ctx, mut act, e) = self {
if serde_json::from_value::<V>(act.clone()).is_ok() {
let act_clone = act.clone();
let act_id = match act_clone["id"].as_str() {
Some(x) => x,
None => return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidID),
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidID),
};
// Get the actor ID
let actor_id = match get_id(act["actor"].clone()) {
Some(x) => x,
None => {
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidActor(None))
}
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(None)),
};
if Self::is_spoofed_activity(&actor_id, &act) {
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidObject(None));
return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None));
}
// Transform this actor to a model (see FromId for details about the from_id function)
let actor = match A::from_id(
ctx,
sender,
&actor_id,
serde_json::from_value(act["actor"].clone()).ok(),
proxy,
@@ -233,25 +235,17 @@ where
if let Some(json) = json {
act["actor"] = json;
}
return Inbox::NotHandled(
ctx,
sender,
act,
InboxError::InvalidActor(Some(e)),
);
return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(Some(e)));
}
};
// Same logic for "object"
let obj_id = match get_id(act["object"].clone()) {
Some(x) => x,
None => {
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidObject(None))
}
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None)),
};
let obj = match M::from_id(
ctx,
sender,
&obj_id,
serde_json::from_value(act["object"].clone()).ok(),
proxy,
@@ -261,24 +255,19 @@ where
if let Some(json) = json {
act["object"] = json;
}
return Inbox::NotHandled(
ctx,
sender,
act,
InboxError::InvalidObject(Some(e)),
);
return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(Some(e)));
}
};
// 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),
}
} else {
// If the Activity type is not matching the expected one for
// this handler, try with the next one.
Inbox::NotHandled(ctx, sender, act, e)
Inbox::NotHandled(ctx, act, e)
}
} else {
self
@@ -289,7 +278,7 @@ where
pub fn done(self) -> Result<R, E> {
match self {
Inbox::Handled(res) => Ok(res),
Inbox::NotHandled(_, _, _, err) => Err(E::from(err)),
Inbox::NotHandled(_, _, err) => Err(E::from(err)),
Inbox::Failed(err) => Err(err),
}
}
@@ -356,7 +345,6 @@ pub trait FromId<C>: Sized {
/// If absent, the ID will be dereferenced.
fn from_id(
ctx: &C,
sender: &dyn Signer,
id: &str,
object: Option<Self::Object>,
proxy: Option<&reqwest::Proxy>,
@@ -365,7 +353,7 @@ pub trait FromId<C>: Sized {
Ok(x) => Ok(x),
_ => match object {
Some(o) => Self::from_activity(ctx, o).map_err(|e| (None, e)),
None => Self::from_activity(ctx, Self::deref(id, sender, proxy.cloned())?)
None => Self::from_activity(ctx, Self::deref(id, proxy.cloned())?)
.map_err(|e| (None, e)),
},
}
@@ -374,42 +362,18 @@ pub trait FromId<C>: Sized {
/// Dereferences an ID
fn deref(
id: &str,
sender: &dyn Signer,
proxy: Option<reqwest::Proxy>,
) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
let mut headers = request::headers();
let url = Url::parse(&id).map_err(|_| (None, InboxError::InvalidID.into()))?;
if !url.has_host() {
return Err((None, InboxError::InvalidID.into()));
}
let host_header_value = HeaderValue::from_str(&url.host_str().expect("Unreachable"))
.map_err(|_| (None, InboxError::DerefError.into()))?;
headers.insert(HOST, host_header_value);
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)
.headers(headers.clone())
.header(
"Signature",
request::signature(sender, &headers, ("get", url.path(), url.query()))
.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
@@ -417,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.
@@ -455,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 = ();
@@ -467,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 {
@@ -490,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 {
@@ -528,11 +545,53 @@ where
#[cfg(test)]
mod tests {
use super::super::sign::{gen_keypair, Error as SignatureError, Result as SignatureResult};
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 {
type Error = ();
@@ -545,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 {
@@ -569,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 = ();
@@ -629,47 +696,10 @@ mod tests {
act
}
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) -> SignatureResult<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(|_| SignatureError())
}
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<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(|_| SignatureError())
}
}
#[test]
fn test_inbox_basic() {
let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Create, MyObject>(None)
.done();
assert!(res.is_ok());
@@ -678,7 +708,7 @@ mod tests {
#[test]
fn test_inbox_multi_handlers() {
let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Announce, MyObject>(None)
.with::<MyActor, Delete, MyObject>(None)
.with::<MyActor, Create, MyObject>(None)
@@ -691,7 +721,7 @@ mod tests {
fn test_inbox_failure() {
let act = serde_json::to_value(build_create()).unwrap();
// Create is not handled by this inbox
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Announce, MyObject>(None)
.with::<MyActor, Like, MyObject>(None)
.done();
@@ -710,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 {
@@ -740,12 +774,12 @@ mod tests {
fn test_inbox_actor_failure() {
let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act.clone())
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
.with::<FailingActor, Create, MyObject>(None)
.done();
assert!(res.is_err());
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act.clone())
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
.with::<FailingActor, Create, MyObject>(None)
.with::<MyActor, Create, MyObject>(None)
.done();
+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;
+54 -8
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 {
@@ -164,12 +187,35 @@ pub fn signature(
)).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;
use crate::activity_pub::sign::{
gen_keypair, Error as SignatureError, Result as SignatureResult, Signer,
};
use crate::activity_pub::sign::{gen_keypair, Error, Result, Signer};
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
use reqwest::header::HeaderMap;
@@ -193,20 +239,20 @@ mod tests {
"mysigner".into()
}
fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
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();
signer.update(to_sign.as_bytes()).unwrap();
signer.sign_to_vec().map_err(|_| SignatureError())
signer.sign_to_vec().map_err(|_| Error())
}
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
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();
verifier.update(data.as_bytes()).unwrap();
verifier.verify(&signature).map_err(|_| SignatureError())
verifier.verify(&signature).map_err(|_| Error())
}
}
+1 -1
View File
@@ -187,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;
}
+12 -25
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,7 +18,7 @@ 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()
@@ -80,19 +77,6 @@ pub fn iri_percent_encode_seg_char(c: char) -> String {
}
}
/**
* 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(),
)
}
#[derive(Debug)]
enum State {
Mention,
@@ -141,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,
);
@@ -334,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));
(
@@ -467,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::*;
+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 -10
View File
@@ -1,39 +1,40 @@
[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"
ammonia = "3.1.2"
bcrypt = "0.10.1"
guid-create = "0.1"
itertools = "0.8.0"
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"
rocket = "0.4.6"
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
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)
+21 -32
View File
@@ -18,8 +18,7 @@ use openssl::{
};
use plume_common::activity_pub::{
inbox::{AsActor, FromId},
sign::{self, Error as SignatureError, Result as SignatureResult},
ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
};
use url::Url;
use webfinger::*;
@@ -153,8 +152,7 @@ impl Blog {
.and_then(|l| {
Blog::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&l.href?,
&l.href.ok_or(Error::MissingApProperty)?,
None,
CONFIG.proxy(),
)
@@ -246,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!(
@@ -275,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)
}
@@ -328,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)
@@ -349,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,
@@ -369,8 +370,6 @@ impl FromId<DbConn> for Blog {
open_registrations: true,
short_description_html: String::new(),
long_description_html: String::new(),
private_key: None,
public_key: None,
},
)
})?;
@@ -384,14 +383,7 @@ impl FromId<DbConn> for Blog {
Media::save_remote(
conn,
icon.object_props.url_string().ok()?,
&User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&owner,
None,
CONFIG.proxy(),
)
.ok()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
)
.ok()
})
@@ -407,14 +399,7 @@ impl FromId<DbConn> for Blog {
Media::save_remote(
conn,
banner.object_props.url_string().ok()?,
&User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&owner,
None,
CONFIG.proxy(),
)
.ok()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
)
.ok()
})
@@ -458,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 {
@@ -481,18 +470,18 @@ impl sign::Signer for Blog {
format!("{}#main-key", self.ap_url)
}
fn sign(&self, to_sign: &str) -> SignatureResult<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(SignatureError::from)
signer.sign_to_vec().map_err(sign::Error::from)
}
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<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(SignatureError::from)
verifier.verify(signature).map_err(sign::Error::from)
}
}
+23 -15
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
@@ -236,7 +247,6 @@ impl FromId<DbConn> for Comment {
})?,
author_id: User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&note.object_props.attributed_to_link::<Id>()?,
None,
CONFIG.proxy(),
@@ -295,13 +305,7 @@ impl FromId<DbConn> for Comment {
.collect::<HashSet<_>>() // remove duplicates (don't do a query more than once)
.into_iter()
.map(|v| {
if let Ok(user) = User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&v,
None,
CONFIG.proxy(),
) {
if let Ok(user) = User::from_id(conn, &v, None, CONFIG.proxy()) {
vec![user]
} else {
vec![] // TODO try to fetch collection
@@ -325,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 {
@@ -353,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()
}
}
+5 -3
View File
@@ -168,7 +168,6 @@ impl FromId<DbConn> for Follow {
fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> {
let actor = User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&follow.follow_props.actor_link::<Id>()?,
None,
CONFIG.proxy(),
@@ -177,7 +176,6 @@ impl FromId<DbConn> for Follow {
let target = User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&follow.follow_props.object_link::<Id>()?,
None,
CONFIG.proxy(),
@@ -185,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 {
@@ -197,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)?;
}
+15 -21
View File
@@ -3,9 +3,7 @@ use activitypub::activity::*;
use crate::{
comments::Comment,
db_conn::DbConn,
follows,
instance::Instance,
likes,
follows, likes,
posts::{Post, PostUpdate},
reshares::Reshare,
users::User,
@@ -49,24 +47,20 @@ impl_into_inbox_result! {
}
pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result<InboxResult, Error> {
Inbox::handle(
conn,
&Instance::get_local().expect("Failed to get local instance"),
act,
)
.with::<User, Announce, Post>(CONFIG.proxy())
.with::<User, Create, Comment>(CONFIG.proxy())
.with::<User, Create, Post>(CONFIG.proxy())
.with::<User, Delete, Comment>(CONFIG.proxy())
.with::<User, Delete, Post>(CONFIG.proxy())
.with::<User, Delete, User>(CONFIG.proxy())
.with::<User, Follow, User>(CONFIG.proxy())
.with::<User, Like, Post>(CONFIG.proxy())
.with::<User, Undo, Reshare>(CONFIG.proxy())
.with::<User, Undo, follows::Follow>(CONFIG.proxy())
.with::<User, Undo, likes::Like>(CONFIG.proxy())
.with::<User, Update, PostUpdate>(CONFIG.proxy())
.done()
Inbox::handle(conn, act)
.with::<User, Announce, Post>(CONFIG.proxy())
.with::<User, Create, Comment>(CONFIG.proxy())
.with::<User, Create, Post>(CONFIG.proxy())
.with::<User, Delete, Comment>(CONFIG.proxy())
.with::<User, Delete, Post>(CONFIG.proxy())
.with::<User, Delete, User>(CONFIG.proxy())
.with::<User, Follow, User>(CONFIG.proxy())
.with::<User, Like, Post>(CONFIG.proxy())
.with::<User, Undo, Reshare>(CONFIG.proxy())
.with::<User, Undo, follows::Follow>(CONFIG.proxy())
.with::<User, Undo, likes::Like>(CONFIG.proxy())
.with::<User, Update, PostUpdate>(CONFIG.proxy())
.done()
}
#[cfg(test)]
+44 -144
View File
@@ -3,29 +3,14 @@ use crate::{
medias::Media,
safe_string::SafeString,
schema::{instances, users},
users::{Role, User},
users::{NewUser, Role, User},
Connection, Error, Result,
};
use activitypub::{actor::Service, CustomObject};
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use openssl::{
hash::MessageDigest,
pkey::{PKey, Private},
rsa::Rsa,
sign,
};
use plume_common::{
activity_pub::{
sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer},
ApSignature, PublicKey,
},
utils::md_to_html,
};
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;
use tracing::warn;
pub type CustomService = CustomObject<ApSignature, Service>;
#[derive(Clone, Identifiable, Queryable)]
pub struct Instance {
@@ -41,8 +26,6 @@ pub struct Instance {
pub default_license: String,
pub long_description_html: SafeString,
pub short_description_html: SafeString,
pub private_key: Option<String>,
pub public_key: Option<String>,
}
#[derive(Clone, Insertable)]
@@ -57,14 +40,15 @@ pub struct NewInstance {
pub default_license: String,
pub long_description_html: String,
pub short_description_html: String,
pub private_key: Option<String>,
pub public_key: Option<String>,
}
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);
@@ -89,13 +73,6 @@ impl Instance {
*LOCAL_INSTANCE.write().unwrap() = Instance::get_local_uncached(conn).ok();
}
pub fn get_locals(conn: &Connection) -> Result<Vec<Instance>> {
instances::table
.filter(instances::local.eq(true))
.load::<Instance>(conn)
.map_err(Error::from)
}
pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> {
instances::table
.filter(instances::local.eq(false))
@@ -103,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())
@@ -265,112 +278,6 @@ impl Instance {
})
.map_err(Error::from)
}
pub fn set_keypair(&self, conn: &Connection) -> Result<()> {
let (pub_key, priv_key) = gen_keypair();
let private_key = String::from_utf8(priv_key).or(Err(Error::Signature))?;
let public_key = String::from_utf8(pub_key).or(Err(Error::Signature))?;
diesel::update(self)
.set((
instances::private_key.eq(Some(private_key)),
instances::public_key.eq(Some(public_key)),
))
.execute(conn)
.and(Ok(()))
.map_err(Error::from)
}
pub fn get_keypair(&self) -> Result<PKey<Private>> {
PKey::from_rsa(Rsa::private_key_from_pem(
self.private_key.clone()?.as_ref(),
)?)
.map_err(Error::from)
}
/// This is experimental and might change in the future.
/// Currently "!" sign is used but it's not decided.
pub fn ap_url(&self) -> String {
ap_url(&format!(
"{}/!/{}",
Self::get_local().unwrap().public_domain,
self.public_domain
))
}
pub fn to_activity(&self) -> Result<CustomService> {
let mut actor = Service::default();
let id = self.ap_url();
actor.object_props.set_id_string(id.clone())?;
actor.object_props.set_name_string(self.name.clone())?;
let mut ap_signature = ApSignature::default();
if self.local {
if let Some(pub_key) = self.public_key.clone() {
let mut public_key = PublicKey::default();
public_key.set_id_string(format!("{}#main-key", id))?;
public_key.set_owner_string(id)?;
public_key.set_public_key_pem_string(pub_key)?;
ap_signature.set_public_key_publickey(public_key)?;
}
};
Ok(CustomService::new(actor, ap_signature))
}
}
impl NewInstance {
pub fn new_local(
conn: &Connection,
public_domain: String,
name: String,
open_registrations: bool,
default_license: String,
) -> Result<Instance> {
let (pub_key, priv_key) = gen_keypair();
Instance::insert(
conn,
NewInstance {
public_domain,
name,
local: true,
open_registrations,
short_description: SafeString::new(""),
long_description: SafeString::new(""),
default_license,
long_description_html: String::new(),
short_description_html: String::new(),
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
public_key: Some(String::from_utf8(pub_key).or(Err(Error::Signature))?),
},
)
}
}
impl Signer for Instance {
fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url())
}
fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
let key = self.get_keypair()?;
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?;
signer.sign_to_vec().map_err(SignatureError::from)
}
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
if self.public_key.is_none() {
warn!("missing public key for {}", self.public_domain);
return Err(SignatureError());
}
let key = PKey::from_rsa(Rsa::public_key_from_pem(
self.public_key.clone().unwrap().as_ref(),
)?)?;
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?;
verifier.verify(&signature).map_err(SignatureError::from)
}
}
#[cfg(test)]
@@ -392,8 +299,6 @@ pub(crate) mod tests {
name: "My instance".to_string(),
open_registrations: true,
public_domain: "plu.me".to_string(),
private_key: None,
public_key: None,
},
NewInstance {
default_license: "WTFPL".to_string(),
@@ -405,8 +310,6 @@ pub(crate) mod tests {
name: "An instance".to_string(),
open_registrations: true,
public_domain: "1plu.me".to_string(),
private_key: None,
public_key: None,
},
NewInstance {
default_license: "CC-0".to_string(),
@@ -418,8 +321,6 @@ pub(crate) mod tests {
name: "Someone instance".to_string(),
open_registrations: false,
public_domain: "2plu.me".to_string(),
private_key: None,
public_key: None,
},
NewInstance {
default_license: "CC-0-BY-SA".to_string(),
@@ -431,8 +332,6 @@ pub(crate) mod tests {
name: "Nice day".to_string(),
open_registrations: true,
public_domain: "3plu.me".to_string(),
private_key: None,
public_key: None,
},
]
.into_iter()
@@ -445,6 +344,7 @@ pub(crate) mod tests {
})
.collect();
Instance::cache_local(conn);
Instance::cache_local_instance_user(conn);
res
}
Executable → Regular
+33 -21
View File
@@ -1,4 +1,3 @@
#![feature(try_trait)]
#![feature(never_type)]
#![feature(proc_macro_hygiene)]
#![feature(box_patterns)]
@@ -17,10 +16,10 @@ extern crate serde_json;
#[macro_use]
extern crate tantivy;
use db_conn::DbPool;
use instance::Instance;
pub use lettre;
pub use lettre::smtp;
use once_cell::sync::Lazy;
use plume_common::activity_pub::{inbox::InboxError, sign};
use plume_common::activity_pub::{inbox::InboxError, request, sign};
use posts::PostEvent;
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
use users::UserEvent;
@@ -68,6 +67,7 @@ pub enum Error {
Url,
Webfinger,
Expired,
UserAlreadyExists,
}
impl From<bcrypt::BcryptError> for Error {
@@ -94,12 +94,6 @@ impl From<diesel::result::Error> for Error {
}
}
impl From<std::option::NoneError> for Error {
fn from(_: std::option::NoneError) -> Self {
Error::NotFound
}
}
impl From<url::ParseError> for Error {
fn from(_: url::ParseError) -> Self {
Error::Url
@@ -166,14 +160,14 @@ impl From<InboxError<Error>> for Error {
}
}
pub type Result<T> = std::result::Result<T, Error>;
impl From<Error> for sign::Error {
fn from(_: Error) -> Self {
Self()
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
/// matching row for a given list of fields.
///
@@ -309,15 +303,31 @@ pub fn ap_url(url: &str) -> String {
format!("https://{}", url)
}
pub fn migrate_data(dbpool: &DbPool) -> Result<()> {
ensure_local_instance_keys(&dbpool.get().unwrap())
pub trait SmtpNewWithAddr {
fn new_with_addr(
addr: (&str, u16),
) -> std::result::Result<smtp::SmtpClient, smtp::error::Error>;
}
fn ensure_local_instance_keys(conn: &Connection) -> Result<()> {
for instance in Instance::get_locals(conn)? {
instance.set_keypair(conn)?;
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))
}
Ok(())
}
#[cfg(test)]
@@ -367,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;
@@ -387,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;
+6 -3
View File
@@ -7,6 +7,7 @@ use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY,
};
@@ -117,7 +118,6 @@ impl FromId<DbConn> for Like {
NewLike {
post_id: Post::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&act.like_props.object_link::<Id>()?,
None,
CONFIG.proxy(),
@@ -126,7 +126,6 @@ impl FromId<DbConn> for Like {
.id,
user_id: User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&act.like_props.actor_link::<Id>()?,
None,
CONFIG.proxy(),
@@ -139,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 {
@@ -150,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,
+30 -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()
{
@@ -208,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 {
@@ -264,21 +265,24 @@ 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(),
content_warning: image.object_props.summary_string().ok(),
owner_id: User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
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(),
@@ -326,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));
+26 -38
View File
@@ -10,11 +10,12 @@ use activitypub::{
CustomObject,
};
use chrono::{NaiveDateTime, TimeZone, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
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::{iri_percent_encode_seg, md_to_html},
@@ -66,15 +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 = Self::ap_url(post.get_blog(conn)?, &post.slug);
let _: Post = post.save_changes(conn)?;
}
let post = Self::last(conn)?;
if post.published {
post.publish_published();
@@ -97,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)?;
@@ -457,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)?;
}
}
@@ -476,7 +477,7 @@ impl Post {
.iter()
.filter(|m| !new_mentions.contains(&m.mentioned_id))
{
m.delete(&conn)?;
m.delete(conn)?;
}
Ok(())
}
@@ -630,28 +631,13 @@ impl FromId<DbConn> for Post {
.into_iter()
.fold((None, vec![]), |(blog, mut authors), link| {
let url = link;
match User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&url,
None,
CONFIG.proxy(),
) {
match User::from_id(conn, &url, None, CONFIG.proxy()) {
Ok(u) => {
authors.push(u);
(blog, authors)
}
Err(_) => (
blog.or_else(|| {
Blog::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&url,
None,
CONFIG.proxy(),
)
.ok()
}),
blog.or_else(|| Blog::from_id(conn, &url, None, CONFIG.proxy()).ok()),
authors,
),
}
@@ -715,7 +701,7 @@ impl FromId<DbConn> for Post {
Post::insert(
conn,
NewPost {
blog_id: blog?.id,
blog_id: blog.ok_or(Error::NotFound)?.id,
slug: Self::slug(&title).to_string(),
title,
content: SafeString::new(&article.object_props.content_string()?),
@@ -774,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 {
@@ -845,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 {
@@ -852,14 +846,8 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
let mut post = Post::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&self.ap_url,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?;
let mut post =
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
if !post.is_author(conn, actor.id)? {
// TODO: maybe the author was added in the meantime
+1 -8
View File
@@ -1,7 +1,6 @@
use crate::{
db_conn::{DbConn, DbPool},
follows,
instance::Instance,
posts::{LicensedArticle, Post},
users::{User, UserEvent},
ACTOR_SYS, CONFIG, USER_CHAN,
@@ -90,13 +89,7 @@ fn fetch_and_cache_followers(user: &Arc<User>, conn: &DbConn) {
match follower_ids {
Ok(user_ids) => {
for user_id in user_ids {
let follower = User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&user_id,
None,
CONFIG.proxy(),
);
let follower = User::from_id(conn, &user_id, None, CONFIG.proxy());
match follower {
Ok(follower) => {
let inserted = follows::Follow::insert(
+6 -3
View File
@@ -7,6 +7,7 @@ use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY,
};
@@ -142,7 +143,6 @@ impl FromId<DbConn> for Reshare {
NewReshare {
post_id: Post::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&act.announce_props.object_link::<Id>()?,
None,
CONFIG.proxy(),
@@ -151,7 +151,6 @@ impl FromId<DbConn> for Reshare {
.id,
user_id: User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&act.announce_props.actor_link::<Id>()?,
None,
CONFIG.proxy(),
@@ -164,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 {
@@ -175,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 -2
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,
@@ -106,8 +116,6 @@ table! {
default_license -> Text,
long_description_html -> Varchar,
short_description_html -> Varchar,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
}
}
@@ -308,6 +316,7 @@ allow_tables_to_appear_in_same_query!(
blogs,
comments,
comment_seers,
email_signups,
follows,
instances,
likes,
-2
View File
@@ -164,8 +164,6 @@ mod tests {
name: random_hex().to_string(),
open_registrations: true,
public_domain: random_hex().to_string(),
private_key: None,
public_key: None,
},
)
.unwrap();
+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(()),
}
}
}
+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..];
}
+51 -80
View File
@@ -22,17 +22,13 @@ use openssl::{
};
use plume_common::{
activity_pub::{
ap_accept_header,
inbox::{AsActor, AsObject, FromId},
sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, 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,6 +202,22 @@ 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
@@ -212,8 +226,7 @@ impl User {
.ok_or(Error::Webfinger)?;
User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
link.href.as_ref()?,
link.href.as_ref().ok_or(Error::Webfinger)?,
None,
CONFIG.proxy(),
)
@@ -230,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)?;
@@ -262,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();
@@ -434,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(
@@ -448,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!(
@@ -470,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"]
@@ -497,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") {
@@ -520,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;
@@ -542,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"]
@@ -727,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)
}
@@ -950,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,
@@ -965,8 +934,6 @@ impl FromId<DbConn> for User {
open_registrations: true,
short_description_html: String::new(),
long_description_html: String::new(),
private_key: None,
public_key: None,
},
)
})?;
@@ -1040,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 {
@@ -1076,18 +1047,18 @@ impl Signer for User {
format!("{}#main-key", self.ap_url)
}
fn sign(&self, to_sign: &str) -> SignatureResult<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(SignatureError::from)
signer.sign_to_vec().map_err(SignError::from)
}
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<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(SignatureError::from)
verifier.verify(signature).map_err(SignError::from)
}
}
@@ -1128,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 "布"

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