Compare commits

...

471 Commits

Author SHA1 Message Date
KitaitiMakoto ec3205b372 Merge pull request 'v0.7.1' (#1006) from v0.7.1 into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1006
2022-01-12 02:10:48 +00:00
Kitaiti Makoto 45119d9a8c (cargo-release) version {{version}} 2022-01-12 10:42:45 +09:00
Kitaiti Makoto 1065078f75 Update translation files 2022-01-12 10:29:50 +09:00
Kitaiti Makoto 0ce904a985 Update translation files 2022-01-12 10:24:14 +09:00
Kitaiti Makoto 254eec8e6a Follow cargo-release update 2022-01-12 10:19:58 +09:00
KitaitiMakoto 0e4cb4f6e1 Merge pull request '[skip ci]Fix # of pull reuqest in changelog' (#1004) from fix-changelog into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1004
2022-01-12 00:37:19 +00:00
Kitaiti Makoto 9b05ac90df [skip ci]Fix # of pull reuqest in changelog 2022-01-12 09:36:32 +09:00
KitaitiMakoto f28a7fa508 Merge pull request 'Add changelogs' (#1003) from changelog into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1003
2022-01-12 00:35:26 +00:00
Kitaiti Makoto 65e95d8998 Add changelogs 2022-01-12 09:34:39 +09:00
KitaitiMakoto 808b8f8e98 Merge pull request 'Fix #1001 Deny access to disabled sign-up strategy' (#1002) from restrict-signup into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1002
2022-01-12 00:33:04 +00:00
Kitaiti Makoto 43b46a8be4 Make email_signups::create return ErrorPage on error 2022-01-12 09:20:20 +09:00
Kitaiti Makoto 9bbfc71fc8 Fix registration openess condition mistake 2022-01-12 09:19:48 +09:00
Kitaiti Makoto 5d58b31f1c Remove unreachable code 2022-01-12 09:00:04 +09:00
Kitaiti Makoto e31a2238fb Respond with error status code when error 2022-01-12 08:58:42 +09:00
Kitaiti Makoto 7de37bc9b7 Hide password sign-up routings when it's disabled 2022-01-12 08:46:11 +09:00
Kitaiti Makoto 13f7734751 Hide email sign-up routings when it's disabled 2022-01-12 08:40:20 +09:00
Kitaiti Makoto b4395bce99 Implement request guard to detect enabled sign-up strategy 2022-01-12 08:39:33 +09:00
Kitaiti Makoto 7c82b08615 Use into() instead of explicitly wrapping return values 2022-01-12 04:36:01 +09:00
Kitaiti Makoto 6498dbfbb7 Reuse form values 2022-01-12 04:28:02 +09:00
Kitaiti Makoto 74254aed4a Move require_logins from plume-common to plume 2022-01-12 04:18:13 +09:00
KitaitiMakoto 8c48abf48e Merge pull request 'Update crates' (#997) from update-crates into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/997
2022-01-07 18:58:18 +00:00
Kitaiti Makoto 8958226604 Upgrade shrinkwraprs 2022-01-08 03:54:38 +09:00
Kitaiti Makoto 005a6db230 Update crates 2022-01-08 03:20:22 +09:00
KitaitiMakoto 4397abd8ab Merge pull request 'Add plume-front.pot' (#994) from front-po into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/994
2022-01-07 15:40:31 +00:00
Kitaiti Makoto e53882f555 Add plume-front.pot 2022-01-08 00:39:17 +09:00
KitaitiMakoto 5d5e61dfa1 Merge pull request 'Update crates' (#993) from update-crates into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/993
2022-01-06 21:45:53 +00:00
Kitaiti Makoto c5c6b70a89 Upgrade ldap3 2022-01-07 06:17:58 +09:00
Kitaiti Makoto 6778a0e943 Remove hyper from plume-common 2022-01-07 06:16:00 +09:00
Kitaiti Makoto 677e238c6d Follow API change of heck 2022-01-07 06:12:15 +09:00
Kitaiti Makoto b0bc2372fa Upgrade heck 2022-01-07 06:12:03 +09:00
Kitaiti Makoto 6a808c7cc5 Upgrade hex 2022-01-07 06:07:07 +09:00
Kitaiti Makoto d53543ccb1 Upgrade base64 2022-01-07 06:06:49 +09:00
Kitaiti Makoto 88d7d54601 Upgrade whatlang 2022-01-07 06:05:20 +09:00
Kitaiti Makoto 0f0c896887 Upgrade itertools 2022-01-07 06:05:05 +09:00
Kitaiti Makoto 65233c0a9a Upgrade ammonia 2022-01-07 05:53:09 +09:00
Kitaiti Makoto 32e1e4788f Upgrade shrinkwraprs 2022-01-07 05:49:06 +09:00
Kitaiti Makoto 181a78876b Remove askama_escape from dependencies of plume-models 2022-01-07 05:47:46 +09:00
Kitaiti Makoto 61d5446113 Use plume_common::escape() instead of askama_escape::escape() directly 2022-01-07 05:47:08 +09:00
Kitaiti Makoto c786569171 Define plume_common::escape() 2022-01-07 05:36:39 +09:00
Kitaiti Makoto d83a75e3f4 Add askama_escape to plume-common 2022-01-07 05:26:32 +09:00
Kitaiti Makoto a6f06559ea Remove rspassword from dependencies 2022-01-07 05:15:07 +09:00
Kitaiti Makoto 2084145dd3 Upgrade multipart 2022-01-07 05:12:29 +09:00
Kitaiti Makoto dd54058516 Upgrade guid-create 2022-01-07 05:09:08 +09:00
Kitaiti Makoto 4056a54d44 Upgrade tracing-subscriber 2022-01-07 05:03:07 +09:00
Kitaiti Makoto 191cd11741 Upgrade dotenv 2022-01-07 04:59:03 +09:00
Kitaiti Makoto 800e74da67 Follow API change of validator 2022-01-07 04:55:49 +09:00
Kitaiti Makoto 237da47950 Upgrade validator 2022-01-07 04:55:41 +09:00
Kitaiti Makoto ec12539fd0 Follow rsass API change 2022-01-07 04:41:20 +09:00
Kitaiti Makoto a537db559b Upgrade rsass 2022-01-07 04:41:11 +09:00
Kitaiti Makoto 2ba158df67 Update crates 2022-01-07 01:41:22 +09:00
KitaitiMakoto c0c066547f Merge pull request 'Update po files' (#991) from po into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/991
2022-01-06 13:24:09 +00:00
Kitaiti Makoto c3f59b14b9 Update po files 2022-01-06 22:14:24 +09:00
KitaitiMakoto 1d06a8f1ad Merge pull request 'Fixes #636 Email sign up feature' (#990) from mail-confirmation into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/990
2022-01-06 12:04:24 +00:00
Kitaiti Makoto efaf1295e9 Suppress clippy 2022-01-06 20:43:40 +09:00
Kitaiti Makoto 4bc9cf3ad1 [skip ci]Complete changelogs 2022-01-06 20:35:34 +09:00
Kitaiti Makoto 1e3851ea69 Execute SQLs for email_signups in transaction 2022-01-06 20:27:55 +09:00
Kitaiti Makoto b6d38536e3 Add email signup feature 2022-01-06 20:18:20 +09:00
Kitaiti Makoto 9b4c678aa9 Make signup token transparent 2022-01-06 18:40:24 +09:00
Kitaiti Makoto a65775d85b Implement EmailSignup 2022-01-05 03:17:58 +09:00
Kitaiti Makoto 192c7677c3 Run migration
% diesel migration run
2022-01-05 00:01:36 +09:00
Kitaiti Makoto 2a31a7b601 Define email_singups table 2022-01-05 00:01:36 +09:00
Kitaiti Makoto 355fd7cb1d Generate create_email_signups_table migration
% diesel migration generate create_email_signups_table
2022-01-05 00:01:36 +09:00
Kitaiti Makoto 40efd73dfc Add config for sign up strategy 2022-01-05 00:01:32 +09:00
KitaitiMakoto 31b144c76d Merge pull request 'Remove unnecessary prefix' (#986) from fix-tag into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/986
2022-01-04 10:17:53 +00:00
Kitaiti Makoto 31a46514cb Remove unnecessary prefix 2022-01-04 19:17:04 +09:00
KitaitiMakoto c8d906eb99 Merge pull request 'Quote version tag' (#985) from fix-action into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/985
2022-01-04 10:11:19 +00:00
Kitaiti Makoto 2895a1c819 Quote version tag 2022-01-04 19:10:37 +09:00
KitaitiMakoto a4a5d08662 Merge pull request 'deploy-tags' (#984) from deploy-tags into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/984
2022-01-04 10:08:31 +00:00
Kitaiti Makoto b97c9d2165 Deploy Docker images with tags 2022-01-04 19:07:47 +09:00
Kitaiti Makoto 5b7e8a69a5 Revert "Deploy tags"
This reverts commit d9a59f1b07.
2022-01-04 19:04:58 +09:00
KitaitiMakoto 9601e99e33 Merge pull request 'Deploy tags' (#983) from deploy-tags into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/983
2022-01-04 09:42:19 +00:00
Kitaiti Makoto d9a59f1b07 Deploy tags 2022-01-04 18:41:06 +09:00
KitaitiMakoto b6a6af906a Merge pull request '[skip ci]Add changelog abourt MAIL_PORT' (#982) from changelog into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/982
2022-01-03 16:07:13 +00:00
Kitaiti Makoto 2c4799ce27 [skip ci]Add changelog abourt MAIL_PORT 2022-01-04 01:06:12 +09:00
KitaitiMakoto b33b19849c Merge pull request 'Fix notification page error' (#981) from fix-notification-page into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/981
2022-01-03 14:29:43 +00:00
Kitaiti Makoto e398f36c57 Add changelog 2022-01-03 23:28:56 +09:00
Kitaiti Makoto ee6064eee8 Don't unwrap() 2022-01-03 23:28:05 +09:00
KitaitiMakoto 9d012c8f3c Merge pull request 'Closes #944 Mail server port configuration' (#980) from mail-server-port into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/980
2022-01-03 10:37:53 +00:00
Kitaiti Makoto 8888dbba0a Initialize SMTP client with port number 2022-01-03 18:09:26 +09:00
Kitaiti Makoto 6f8d5c1eb4 Add SmtpClient::new_with_addr() method 2022-01-03 18:09:13 +09:00
Kitaiti Makoto 62da4a3d5c Add native-tls to plume-models' dependencies 2022-01-03 18:04:49 +09:00
Kitaiti Makoto 5cfc8e71a5 Remove Lettre from plume module dependencies 2022-01-03 17:36:37 +09:00
Kitaiti Makoto a599760891 Use smtp module from plume_models instead of lettre directly 2022-01-03 17:36:11 +09:00
Kitaiti Makoto 00324f668f Add port field to MailConfig 2022-01-03 17:25:46 +09:00
Kitaiti Makoto d4549704b9 Install Lettre 2022-01-03 17:11:35 +09:00
Kitaiti Makoto 0836e3d693 Add Lettre to plume-models' dependencies 2022-01-03 17:09:47 +09:00
Kitaiti Makoto 0058c3053d Move mail config from plume::mail::mailer to plume_models::CONFIG 2022-01-03 15:50:04 +09:00
KitaitiMakoto 2a1a0a23a5 Merge pull request 'Move bottombar styles to _article.scss' (#978) from bottombar-styling into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/978
2022-01-02 18:03:46 +00:00
Kitaiti Makoto 5614e3bd59 Move bottombar styles to _article.scss 2022-01-03 03:01:59 +09:00
KitaitiMakoto acbda3cde1 Merge pull request 'Make bottom bar smaller in narrow window' (#977) from layout-post-control into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/977
2022-01-02 17:50:20 +00:00
Kitaiti Makoto 0755436458 Make bottom bar smaller in narrow window 2022-01-03 02:49:45 +09:00
KitaitiMakoto 3daf405ae2 Merge pull request 'Make blog cover clickable' (#976) from clickable-blog-image into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/976
2022-01-02 17:40:30 +00:00
Kitaiti Makoto 53dc3b0c03 Fix cover size of posts 2022-01-03 02:39:17 +09:00
Kitaiti Makoto 371dcc5091 Address blog title positoin in dashboard 2022-01-03 02:37:45 +09:00
Kitaiti Makoto 905fe54fa3 Make blog cover a link 2022-01-03 02:35:07 +09:00
Kitaiti Makoto 62c0827ff5 Remove needless whitespaces 2022-01-03 02:32:17 +09:00
KitaitiMakoto d6c65ce81a Merge pull request 'Fix #927 Ensure Post ap_url' (#975) from ensure-ap-url into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/975
2022-01-02 17:24:04 +00:00
Kitaiti Makoto 5532b4a4d7 Ensure Post ap_url 2022-01-03 02:21:34 +09:00
KitaitiMakoto 637bd3347b Merge pull request 'Fix #967 Fix comment link' (#974) from fix-comment-link into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/974
2022-01-02 17:08:10 +00:00
Kitaiti Makoto bac373a818 Fix comment link 2022-01-03 02:06:59 +09:00
Kitaiti Makoto f0e7ea5640 (cargo-release) version {{version}} 2022-01-03 01:17:07 +09:00
Kitaiti Makoto 4b981e0fad (cargo-release) version {{version}} 2022-01-03 00:56:29 +09:00
KitaitiMakoto 8fb9d861de Merge pull request '[skip ci]Give up to deploy tags' (#973) from giveup-tag into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/973
2022-01-02 15:43:05 +00:00
Kitaiti Makoto 199269ba3c [skip ci]Give up to deploy tags 2022-01-03 00:42:36 +09:00
KitaitiMakoto 0b9ec4c52c Merge pull request 'Use Git tag for Docker image tag' (#972) from deploy-tag into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/972
2022-01-02 15:14:38 +00:00
Kitaiti Makoto 0e51565cc8 Use Git tag for Docker image tag 2022-01-03 00:14:00 +09:00
KitaitiMakoto 0418d35b67 Merge pull request 'Fix a typo' (#971) from deploy-tag into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/971
2022-01-02 15:06:26 +00:00
Kitaiti Makoto bf9d25363b Fix a typo 2022-01-03 00:04:57 +09:00
KitaitiMakoto 84f00c57d1 Merge pull request 'Fix tag calculation' (#970) from deploy-tag into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/970
2022-01-02 15:02:15 +00:00
Kitaiti Makoto 7c1a5421fa Fix tag calculation 2022-01-03 00:01:18 +09:00
KitaitiMakoto 3815bfe980 Merge pull request '[skip ci]Deploy tags to Docker Hub' (#969) from deploy-tag into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/969
2022-01-02 14:51:03 +00:00
Kitaiti Makoto de448c3192 [skip ci]Deploy tags to Docker Hub 2022-01-02 23:49:42 +09:00
KitaitiMakoto 967e2dfde6 Merge pull request 'Add GitHub Action to deploy Docker image' (#968) from gh-action into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/968
2022-01-02 14:28:27 +00:00
Kitaiti Makoto dd3c1eac5f Add GitHub Action to deploy Docker image 2022-01-02 23:26:39 +09:00
KitaitiMakoto abc0a794c1 Merge pull request 'v0.7.0' (#923) from v0.7.0 into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/923
2022-01-02 11:48:46 +00:00
Kitaiti Makoto ef628aa498 (cargo-release) version {{version}} 2022-01-02 20:41:28 +09:00
Kitaiti Makoto 011bd9602d Update plume-buildenv Docker image to v0.4.0 2021-12-12 00:54:26 +09:00
Kitaiti Makoto b0745cfd82 Fix search string in release.toml 2021-12-12 00:38:22 +09:00
Kitaiti Makoto d1b1d9f507 Update translations 2021-12-12 00:01:11 +09:00
Kitaiti Makoto 742d545f1c Update po files 2021-12-11 23:57:05 +09:00
Kitaiti Makoto 67dec4df22 Merge remote-tracking branch 'origin/main' into v0.7.0 2021-12-09 14:35:10 +09:00
KitaitiMakoto 33f3bbb774 Merge pull request 'Proper scaling of the default image, when a user doesn't have custom avatar' (#965) from mareklach/Plume:proper_scaling_for_blank_user_avatar into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/965
2021-12-09 05:33:52 +00:00
mareklach 7c1df80695 Update 'assets/themes/default/_global.scss' 2021-12-06 15:15:28 +00:00
mareklach 2adbb6f74c Merge branch 'main' into proper_scaling_for_blank_user_avatar 2021-12-06 14:52:28 +00:00
mareklach 79c05e8381 Proper scaling of the default image, when a user doesn't have custom avatar 2021-12-06 14:48:05 +00:00
Kitaiti Makoto 31a1591043 Merge remote-tracking branch 'origin/main' into v0.7.0 2021-12-06 07:32:05 +09:00
KitaitiMakoto 0ede2ab3ab Merge pull request 'Fixes #936: Sign GET request' (#957) from sign-get into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/957
2021-12-05 22:13:24 +00:00
Kitaiti Makoto 43656d8e46 Create instance for test 2021-12-06 00:33:48 +09:00
Kitaiti Makoto b7cc2369a7 Allow clippy::needless_borrow on CI 2021-12-06 00:33:20 +09:00
Kitaiti Makoto 05a98175bf Assign unsed return value to underscore variable 2021-12-05 21:56:00 +09:00
Kitaiti Makoto 346a67fe1c Create local instance user on start 2021-12-05 21:34:36 +09:00
Kitaiti Makoto a22f5f2336 Add more changelogs 2021-12-05 21:20:37 +09:00
Kitaiti Makoto 55439990c6 Complete changelog 2021-12-05 21:12:50 +09:00
Kitaiti Makoto 3b0f28d061 Update po files 2021-12-05 21:12:14 +09:00
Kitaiti Makoto ac3acfb4ac Merge latest translations 2021-12-05 21:12:14 +09:00
Kitaiti Makoto d5256e9ffc Merge translations from Crowdin 2021-12-05 21:12:14 +09:00
Kitaiti Makoto 13be46445c Add changelog about signing GET requests 2021-12-05 21:10:22 +09:00
Kitaiti Makoto c67b702425 Cache LOCAL_INSTANCE_USER once 2021-12-05 20:57:10 +09:00
Kitaiti Makoto e01539ef16 Follow Clippy 2021-12-05 20:24:11 +09:00
Kitaiti Makoto de4380fd34 Cache local when creating local instance 2021-12-05 20:22:32 +09:00
Kitaiti Makoto 5651e11ba1 Add FIXME comment 2021-12-05 20:22:10 +09:00
KitaitiMakoto 5815602309 Merge pull request 'Don't shrink edit link' (#964) from edit-link-no-shrink into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/964
2021-12-05 10:38:49 +00:00
Kitaiti Makoto a0a69dfb22 Don't shrink edit link 2021-12-05 19:37:38 +09:00
Kitaiti Makoto d98132db80 Don't log unnecessary error 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 25fe2ad802 Use request::get() instead of ClientBuilder 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 48fab8ad2c Impl From for request::Error 2021-12-05 19:32:27 +09:00
Kitaiti Makoto a7d8d49faf Define request::get() function 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 388acd6738 Remove needless reference sign 2021-12-05 19:32:27 +09:00
Kitaiti Makoto d3c035aa39 Remove needless code 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 12a8d00f8e Cache local instance user for test 2021-12-05 19:32:27 +09:00
Kitaiti Makoto b9ea06a01a Don't stop even when caching local instance user 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 1e67b3c13c Create local instance user on caching if it doesn't exist 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 5a5c8bdac8 Make inbox test follow API change 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 44f9d36df1 Make request test follow API change 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 7d349c2de6 Install once_cell 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 79715ec7c7 Add once_cell to dev-dependencies for plume-common 2021-12-05 19:32:27 +09:00
Kitaiti Makoto f4d7dfb261 Sign GET request to other instances 2021-12-05 19:32:27 +09:00
Kitaiti Makoto c525410062 Create local instance user on creating instance 2021-12-05 19:32:27 +09:00
Kitaiti Makoto af5b0b961b Extract Instance::create_local_instance_user() from get_local_instance_user_uncached() 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 2f7a5cbf56 Cache local instance user on start 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 1506802c20 Implement LOCAL_INSTANCE_USER and related methods 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 897ea8e11e Change const name: LOCAL_INSTANCE_USER -> LOCAL_INSTANCE_USERNAME 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 858806149a Use concrete Error for Signer 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 0da9572627 Implement User::get_local_instance_user() 2021-12-05 19:32:27 +09:00
Kitaiti Makoto 94cc260803 Add Instance to users::Role 2021-12-05 19:32:27 +09:00
KitaitiMakoto 7198b06a33 Merge pull request 'Fix problem of PL #956' (#963) from mskf1383/Plume:main into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/963
Reviewed-by: KitaitiMakoto <kitaitimakoto@noreply@joinplu.me>
2021-12-05 10:12:48 +00:00
MohammadSaleh Kamyab 81006e1db8 Update 'assets/themes/default/_global.scss' 2021-12-02 13:16:14 +00:00
KitaitiMakoto 0f7094a70e Merge pull request 'Update crates' (#961) from update-crates into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/961
2021-11-28 00:54:35 +00:00
Kitaiti Makoto a0b661fffe Make template follow change of Rust 2021-11-28 08:48:59 +09:00
Kitaiti Makoto 4bef91f08b Follow clippy warnings 2021-11-28 08:36:27 +09:00
Kitaiti Makoto 20c17be124 Update Rust version 2021-11-28 08:13:16 +09:00
Kitaiti Makoto ae3344f318 Follow Rust and crates update 2021-11-28 08:06:29 +09:00
Kitaiti Makoto 9187e4dde9 Update crates 2021-11-28 08:06:29 +09:00
Kitaiti Makoto 89c185f819 Specify Rocket and serde_json version loosely 2021-11-28 08:06:29 +09:00
Kitaiti Makoto 09b9a37720 Update Rust version 2021-11-28 08:06:24 +09:00
KitaitiMakoto 12a8bfcf2d Merge pull request 'Fix article title overflowing on small screens' (#960) from mareklach/Plume:fix-article-title-overflow-spill into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/960
2021-11-26 19:53:40 +00:00
mareklach b2be00b125 Fix article title overflowing on small screens
Improves CSS scaling of the article title for mobile-sized screens, to prevent it spilling over.
2021-11-26 16:50:08 +00:00
KitaitiMakoto 914d394bd0 Merge pull request 'Stick serde_json versoin for plume-models' (#959) from stick-serde_json into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/959
2021-11-25 23:55:37 +00:00
Kitaiti Makoto 811db1be0e Stick serde_json versoin for plume-models 2021-11-26 08:53:55 +09:00
KitaitiMakoto cd81f042b9 Merge pull request 'Stick serde_json to < 1.0.70' (#958) from stick-serde_json into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/958
2021-11-25 23:35:31 +00:00
Kitaiti Makoto c6111fcd28 Stick serde_json to < 1.0.70 2021-11-26 08:34:38 +09:00
KitaitiMakoto 82ebdc023c Merge pull request 'Fix RTL problem in post card' (#956) from mskf1383/Plume:main into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/956
Reviewed-by: KitaitiMakoto <kitaitimakoto@noreply@joinplu.me>
2021-11-21 14:32:14 +00:00
MohammadSaleh Kamyab 7b110179a9 Update 'assets/themes/default/_global.scss' 2021-11-21 12:21:47 +00:00
MohammadSaleh Kamyab afd66ce7cb Update 'assets/themes/default/_global.scss' 2021-11-21 05:46:48 +00:00
MohammadSaleh Kamyab 9dac97045b Update 'templates/partials/post_card.rs.html' 2021-11-21 05:37:57 +00:00
KitaitiMakoto 188d4ac063 Merge pull request 'Post ap_url as valid IRI' (#947) from post-slug into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/947
2021-09-07 05:36:00 +00:00
Kitaiti Makoto 0fbefe2cdc Percent-encode Post slug on sending Activity to other instances 2021-09-07 13:31:06 +09:00
Kitaiti Makoto eedd5fe4e9 Define utility function to percent-encode for IRI 2021-09-06 17:49:10 +09:00
KitaitiMakoto 27a1a56223 Merge pull request 'Make Feature list more clear' (#946) from github-856 into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/946
2021-09-05 12:50:49 +00:00
Kitaiti Makoto 8e3322776a Merge remote-tracking branch 'origin/main' into github-856 2021-09-05 21:49:50 +09:00
KitaitiMakoto 3d4336c548 Merge pull request 'update some dependancies' (#945) from update-deps into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/945
Reviewed-by: trinity-1686a <trinity-1686a@noreply@joinplu.me>
2021-09-05 12:48:07 +00:00
Trinity Pointard 0c8c607aa4 update some dependancies 2021-09-05 14:41:36 +02:00
D5k H3h 60d6734fe2 Make Feature list more clear
Say it, when something is currently missing.
Otherwise, people will get confused.
2021-09-05 11:58:02 +02:00
KitaitiMakoto 8c372aa6fc Merge pull request 'Fixes #929 Don't stip shipped binaries' (#942) from dont-strip into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/942
2021-08-15 15:46:02 +00:00
Kitaiti Makoto d1a74ca8e6 Don't stip shipped binaries 2021-08-16 00:44:15 +09:00
KitaitiMakoto c374e0af4c Merge pull request 'Upgrade bcrypt create' (#941) from update-blowfish into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/941
2021-08-14 10:23:41 +00:00
Kitaiti Makoto 58e8569048 Install bcrypt 2021-08-14 19:20:30 +09:00
Kitaiti Makoto 83dbf2a945 Upgrade bcrypt 2021-08-14 19:20:21 +09:00
trinity-1686a d7d6d5f644 Merge pull request 'Draw side line for blockquote on start' (#933) from ahangarha/Plume:ahangarha-patch-1-blockquote-line into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/933
Reviewed-by: trinity-1686a <trinity-1686a@noreply@joinplu.me>
2021-07-13 10:16:26 +00:00
trinity-1686a 9aa9885a89 Merge pull request 'Specify rocket version' (#937) from zynnnn/Plume:fix-rocket-version into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/937
Reviewed-by: trinity-1686a <trinity-1686a@noreply@joinplu.me>
2021-07-13 10:15:53 +00:00
Trinity Pointard 7fe1e083e0 use newer pulldown-cmark 2021-07-10 13:34:13 +02:00
Tdxdxoz a946823554 Specify rocket version 2021-07-06 22:36:59 +08:00
ahangarha 3a1872c03e Draw side line for blockquote on start
By this change, the line beside blockquote would appear at start. It means if the text is in LTR language, it appears on left and if it is in RTL, the line appears on right.
2021-06-11 11:53:28 +00:00
KitaitiMakoto 5424f9110f Merge pull request 'Add changelog about change of slug' (#921) from changelog into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/921
2021-04-11 02:45:56 +00:00
Kitaiti Makoto a597816617 Add changelog about change of slug 2021-04-11 11:44:52 +09:00
KitaitiMakoto fa7a44f6bb Merge pull request 'Fix #326 and #721: Keep title in URI' (#920) from keep-title-in-uri into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/920
2021-04-10 08:27:31 +00:00
Kitaiti Makoto dfcdcc1833 Remove heck from plume and plume-models 2021-04-10 16:39:23 +09:00
Kitaiti Makoto 6345a57498 Don't modify article title for slug 2021-04-10 16:37:26 +09:00
Kitaiti Makoto 87457c0ed1 Define Post::slug() 2021-04-10 16:30:50 +09:00
KitaitiMakoto 859a1fd528 Merge pull request 'Set default RUST_LOG to info' (#919) from default-log-level into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/919
2021-04-10 07:04:53 +00:00
Kitaiti Makoto 5069aab584 Set default RUST_LOG to info 2021-04-10 15:59:15 +09:00
KitaitiMakoto 033fde38fe Merge pull request 'Extract Post::ap_url()' (#918) from ap_url into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/918
2021-04-09 08:41:04 +00:00
Kitaiti Makoto 7b2bab0f9d Extract Post::ap_url() 2021-04-09 10:55:09 +09:00
KitaitiMakoto a419ef5319 Merge pull request 'Prevent duplicated posts in 'all' timeline' (#917) from dup-timeline into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/917
2021-03-29 15:31:52 +00:00
Kitaiti Makoto f77dce9f12 Add changelog about fix to duplicated posts in all timeline 2021-03-29 23:35:31 +09:00
Kitaiti Makoto 4c2cd92f0d Prevent duplicated posts in 'all' timeline 2021-03-29 23:33:57 +09:00
KitaitiMakoto dd932e1f15 Merge pull request 'Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured' (#916) from media-directory into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/916
2021-03-28 18:18:14 +00:00
Kitaiti Makoto 4376810d96 Add changelog about fix of MEDIA_UPLOAD_DIRECTORY handling 2021-03-29 02:32:02 +09:00
Kitaiti Makoto c961f4751b Complete pull request number to changelog 2021-03-29 02:29:50 +09:00
Kitaiti Makoto 9e5ec0c9df Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured 2021-03-29 02:29:15 +09:00
KitaitiMakoto 112c034e27 Merge pull request 'Update Post.ap_url when published' (#915) from update-ap-url into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/915
2021-03-28 17:27:45 +00:00
Kitaiti Makoto 664a3ddeea Add changelog about update of ap_url 2021-03-29 02:26:30 +09:00
Kitaiti Makoto 2ffd357d95 Update Post.ap_url when published 2021-03-29 01:38:28 +09:00
KitaitiMakoto 2bc4a13964 Merge pull request 'Add migration to add index to medias.file_path for SQLite3' (#914) from upsert-sqlite into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/914
2021-03-28 14:22:10 +00:00
Kitaiti Makoto 58324945cc Add migration to add index to medias.file_path for SQLite3 2021-03-28 17:34:41 +09:00
KitaitiMakoto 16953ea907 Merge pull request 'Upsert posts and media when fetching remote posts' (#912) from upsert into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/912
2021-03-27 19:27:59 +00:00
Kitaiti Makoto d3c2dc8286 Add changelog on upsert 2021-03-28 04:27:06 +09:00
Kitaiti Makoto ebb0b45299 Create blog_authors only when needed 2021-03-28 04:26:17 +09:00
Kitaiti Makoto 702aa11ecf Don't drop path components of image URI 2021-03-28 04:26:17 +09:00
Kitaiti Makoto 33221d386e Upsert Post in from_activity() 2021-03-28 04:26:17 +09:00
Kitaiti Makoto 589c159eb9 Call Post::publish_updated() only for local posts 2021-03-28 04:26:17 +09:00
Kitaiti Makoto ea1f4d48d5 Upsert Media in from_activity() 2021-03-28 04:26:17 +09:00
Kitaiti Makoto 462c5a1d42 Define Media::find_by_file_path() 2021-03-28 04:26:17 +09:00
Kitaiti Makoto f90d7ddee3 Add SQL to add/drop index to/from medias.file_path 2021-03-28 04:26:17 +09:00
Kitaiti Makoto 175055cf9d Add migration to add index to medias.file_path
% diesel migration generate medias_index_file_path
2021-03-28 04:26:17 +09:00
KitaitiMakoto fe92d95f6c Merge pull request 'Make actors subscribe to channel once' (#913) from remote-fetch-once into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/913
2021-03-27 19:25:22 +00:00
Kitaiti Makoto 8e50d95a7a Fix code according to clippy 2021-03-28 03:46:37 +09:00
Kitaiti Makoto 9ed36b2aa3 Add changelog on actor subscription fix 2021-03-28 03:12:20 +09:00
Kitaiti Makoto 722165a734 Upgrade lexical-core crate 2021-03-28 01:39:41 +09:00
Kitaiti Makoto 74f99e2588 Upgrade Rust version 2021-03-28 01:39:26 +09:00
Kitaiti Makoto 77c08845b5 [BUG FIX]Make SearchActor subscribe to channel once 2021-03-27 23:55:26 +09:00
Kitaiti Makoto 2bfc26faf2 [BUG FIX]Make RemoveFetchActor subscribe to channel once 2021-03-27 23:54:09 +09:00
KitaitiMakoto c26f2623a8 Merge pull request 'Fix #905 Prevent remote image duplication' (#907) from dup-remote-image into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/907
2021-02-18 06:10:27 +00:00
Kitaiti Makoto 451c3b5c88 Add TODO comments 2021-02-18 11:35:39 +09:00
Kitaiti Makoto af5674074d Revert "Save remote post only when it doesn't exist on local"
This reverts commit 37cbf537bb.
2021-02-18 11:34:20 +09:00
Kitaiti Makoto 28380021a7 Determine file path of mirror of remote image using the URI 2021-02-18 01:23:57 +09:00
Kitaiti Makoto 37cbf537bb Save remote post only when it doesn't exist on local 2021-02-16 23:48:09 +09:00
KitaitiMakoto 9345a1b7a9 Merge pull request 'Make images in post card links' (#900) from image-link into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/900
2021-02-15 01:42:21 +00:00
Kitaiti Makoto d18f1d6d1d Indicate images in post card clickable 2021-02-15 10:17:41 +09:00
Kitaiti Makoto 621258481d Make images in post card link 2021-02-15 10:17:22 +09:00
KitaitiMakoto ce285bac17 Merge pull request 'Make links in menu work on iOS' (#899) from menu into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/899
2021-02-14 15:02:03 +00:00
KitaitiMakoto a7e45bee11 Merge pull request 'Make it possible to open menu on iOS mobile devices' (#897) from menu into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/897
2021-02-14 14:56:55 +00:00
KitaitiMakoto 73e075e216 Merge pull request 'Fix #890 Fix interact URI used from remote account to remote account' (#898) from remote-follow-from-remote into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/898
2021-02-14 14:40:25 +00:00
Kitaiti Makoto b7c7b6da9f Percent encode remote interact URI 2021-02-14 22:59:01 +09:00
Kitaiti Makoto 703328601c [BUG FIX] Don't append host part twice to remote interact URI 2021-02-14 22:15:32 +09:00
Kitaiti Makoto b2e7664339 Extract User::acct_authority() method 2021-02-14 22:14:44 +09:00
Kitaiti Makoto ed22c6b103 Make links in menu work on iOS 2021-02-13 23:40:18 +09:00
Kitaiti Makoto 94ccbd6190 Move Rust flag for plume-front to .cargo/config.toml from env var 2021-02-13 22:53:20 +09:00
Kitaiti Makoto 77edc4849f Add current pull request to changelog 2021-02-13 22:23:03 +09:00
Kitaiti Makoto 07da97f434 Revert "Remove unused diesel_cli from Docker env image"
This reverts commit 056d68804a.
2021-02-13 19:36:57 +09:00
Kitaiti Makoto 89dd6be7dd Fix design 2021-02-12 19:28:45 +09:00
Kitaiti Makoto 60c1a4264e Fix indentation 2021-02-12 19:21:36 +09:00
Kitaiti Makoto c4c177c530 Remove unnecessary CSS properties 2021-02-12 19:21:02 +09:00
Kitaiti Makoto 4a5bffbd57 Update base Docker image for buildenv 2021-02-12 19:10:54 +09:00
Kitaiti Makoto 49b2296015 Revert "Install rustfmt and cargo-clippy explicitly in dev env"
This reverts commit 347bc653d8.
2021-02-12 19:08:33 +09:00
Kitaiti Makoto 0ef4a06565 Revert "Install zip package in Docker dev image"
This reverts commit b55c2ff2f0.
2021-02-12 19:07:26 +09:00
Kitaiti Makoto 16b1337d67 Add WAI-ARIA attributes to menu 2021-02-12 18:54:42 +09:00
Kitaiti Makoto 6599179174 Use document() function 2021-02-12 18:48:33 +09:00
Kitaiti Makoto f2411f4607 Use a element instead of nav to capture touchend event 2021-02-12 18:26:05 +09:00
Kitaiti Makoto 2c3d429b8e Suppress Clippy 2021-02-12 18:20:48 +09:00
Kitaiti Makoto 7562cf1e75 Open menu even when focus-within code doesn't work 2021-02-12 18:18:45 +09:00
Kitaiti Makoto de1c3f3245 Move document() from plume_front::editor to plume_front 2021-02-12 18:12:38 +09:00
Kitaiti Makoto 5d38c3ad70 Fix file name 2021-02-12 18:11:49 +09:00
Kitaiti Makoto b55c2ff2f0 Install zip package in Docker dev image 2021-02-12 14:38:15 +09:00
Kitaiti Makoto 056d68804a Remove unused diesel_cli from Docker env image 2021-02-12 14:35:06 +09:00
Kitaiti Makoto 347bc653d8 Install rustfmt and cargo-clippy explicitly in dev env 2021-02-12 14:34:32 +09:00
Kitaiti Makoto 6fcece3bc0 Follow file name change 2021-02-12 13:31:22 +09:00
Kitaiti Makoto e119c5bde9 Update Docker image for testing 2021-02-12 13:10:32 +09:00
Kitaiti Makoto 1bc2749e1b Return when required element is not present 2021-02-12 05:00:42 +09:00
Kitaiti Makoto 58bb07d11d Remove unused stdweb crate from dependencies 2021-02-12 05:00:42 +09:00
Kitaiti Makoto affe24b9c6 Replace cargo-web with wasm-pack on build environment 2021-02-12 05:00:34 +09:00
Kitaiti Makoto 0bcf063b05 Remove old plume_front::main 2021-02-12 03:20:33 +09:00
Kitaiti Makoto 24ecb15119 Remplement plume_front::editor using web-sys 2021-02-12 03:19:46 +09:00
Kitaiti Makoto 208c515d70 Remove prefix 'web_sys' 2021-02-11 01:24:41 +09:00
Kitaiti Makoto d0ada7fc53 Reimplement search() in web-sys 2021-02-11 01:19:42 +09:00
Kitaiti Makoto d86bd898cb Prepare JavaScript on build process 2021-02-11 01:19:32 +09:00
Kitaiti Makoto db581a955b Reimplement menu() in web-sys 2021-02-10 23:51:09 +09:00
Kitaiti Makoto 0b8b1bf25f Load new JavaScript file 2021-02-10 23:50:41 +09:00
Kitaiti Makoto faab5f6b3b Make plume-front's type cdylib 2021-02-10 21:53:59 +09:00
Kitaiti Makoto 6387a1334b Add wasm-bindgen to plume-front's dependencies 2021-02-10 21:51:57 +09:00
KitaitiMakoto 10be055381 Merge pull request 'Fix #892 Build client for every inbox in broadcast()' (#895) from broadcast-timeout into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/895
2021-02-09 13:56:16 +00:00
Kitaiti Makoto 24c9c7b5a1 Build client for every inbox in broadcast() 2021-02-09 18:15:14 +09:00
KitaitiMakoto ac48744aca Merge pull request 'Fix #872 Include (request-target) and Host header to HTTP Signature' (#884) from httpsig into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/884
2021-02-06 19:19:36 +00:00
KitaitiMakoto f2d2ecb247 Merge pull request 'Raise jobs on CI' (#893) from ci-jobs into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/893
2021-02-06 19:18:21 +00:00
Kitaiti Makoto 1d46bf8d50 Reduce start jobs on CI 2021-02-07 03:28:07 +09:00
Kitaiti Makoto 4353f2a92d raise jobs on CI 2021-02-07 03:13:38 +09:00
Kitaiti Makoto 1b9aeae53c Add test for signagure() 2021-02-05 10:34:33 +09:00
Kitaiti Makoto 540b9af873 Add changelog about (request-target) 2021-02-04 22:14:25 +09:00
Kitaiti Makoto d08d2c6958 Care the case of invalid header value 2021-02-04 17:10:05 +09:00
Kitaiti Makoto 348259a14b Care about error case 2021-02-04 15:31:57 +09:00
KitaitiMakoto e68ca1151d Merge pull request 'Close #888 Extract DbConn from PlumeRocket' (#889) from extract-dbconn into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/889
2021-01-31 17:32:46 +00:00
Kitaiti Makoto 3c15f5a82f Merge branch 'main' into extract-dbconn 2021-02-01 02:31:45 +09:00
Kitaiti Makoto 5ba823990a Don't panic in loop 2021-02-01 01:36:36 +09:00
Kitaiti Makoto c82c38fe52 Comment on database connections 2021-02-01 01:16:52 +09:00
Kitaiti Makoto e7ab80d5c8 Move worker tasks in routes::user::details to RemoteFetchActor 2021-02-01 00:51:37 +09:00
Kitaiti Makoto 61b68c3a42 Initialize RemoteFetchActor 2021-02-01 00:50:48 +09:00
Kitaiti Makoto 6401cb6fb7 Define User::remote_user_found() and publish_remote_user_found() 2021-02-01 00:48:22 +09:00
Kitaiti Makoto 9d37408535 Define RemoteFetchActor 2021-01-31 22:56:17 +09:00
Kitaiti Makoto ded64e1cf0 Add USER_CHAN 2021-01-31 22:55:52 +09:00
Kitaiti Makoto 3b766fc427 Define UserEvent 2021-01-31 22:55:28 +09:00
Kitaiti Makoto ad285898f6 Replace PlumeRocket.conn with DbConn 2021-01-31 21:58:44 +09:00
Kitaiti Makoto 78be49d57d Make tests follow API changes 2021-01-31 21:58:44 +09:00
Kitaiti Makoto 25e52788c5 Replace PlumeRocket with DbConn where possible 2021-01-31 21:58:09 +09:00
Kitaiti Makoto 5d25a64f13 Remove DbConn from PlumeRocket 2021-01-31 21:58:09 +09:00
Kitaiti Makoto 0dc89fbf23 Set default log level to warn 2021-01-31 21:58:03 +09:00
KitaitiMakoto 94ed0eec2a Merge pull request 'Set default log level to warn' (#886) from loglevel-warn into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/886
2021-01-25 07:40:09 +00:00
Kitaiti Makoto d70e585b64 Set default log level to warn 2021-01-25 16:39:25 +09:00
KitaitiMakoto 8f55e50c6e Merge pull request 'Add default log level in .env.sample' (#885) from loglevel into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/885
2021-01-25 01:43:19 +00:00
Kitaiti Makoto c113c99e1d Add default log level in .env.sample 2021-01-25 09:41:40 +09:00
Kitaiti Makoto 191239584b Define RequestTarget 2021-01-25 02:16:53 +09:00
Kitaiti Makoto bd1caaf5da Include (request-target) and Host header to HTTP Signature 2021-01-25 02:16:53 +09:00
KitaitiMakoto 76f7b5e7ac Merge pull request 'Add shortcut links to edit page' (#883) from shortcut-links into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/883
2021-01-24 17:16:11 +00:00
Kitaiti Makoto afe449b951 Set ROCKET_SECRET_KEY on CI 2021-01-25 01:25:37 +09:00
Kitaiti Makoto 29f0712ea1 Init search index on CI 2021-01-25 00:46:26 +09:00
Kitaiti Makoto d091c24f79 Specify cpu count to build plume-cli 2021-01-25 00:27:45 +09:00
Kitaiti Makoto f9e7d2bb23 Remove all instances before testing 2021-01-25 00:06:40 +09:00
Kitaiti Makoto dbf36ddf05 Cache fetched instance 2021-01-25 00:06:40 +09:00
Kitaiti Makoto 8b00853e88 Make instance for search actor remote 2021-01-25 00:06:35 +09:00
Kitaiti Makoto 84a3d53075 Use uri! macro to generate URI strings 2021-01-24 23:30:24 +09:00
Kitaiti Makoto 7967dd91c2 Remove needless option; duplicated to env var 2021-01-24 23:30:24 +09:00
Kitaiti Makoto b5eb2628f9 Don't remove records after testing 2021-01-24 23:30:20 +09:00
Kitaiti Makoto 4ef6561cc6 Extract login() from test function 2021-01-24 21:38:51 +09:00
Kitaiti Makoto 7f54f418a8 Suppress clippy 2021-01-24 19:49:49 +09:00
Kitaiti Makoto 8f90ea18bf Add tests for edit link presence 2021-01-24 19:37:49 +09:00
Kitaiti Makoto e12528aae4 Extract launch_rocket() from main() as public within crate scope 2021-01-24 19:28:05 +09:00
Kitaiti Makoto 042a1fd2b0 Use Post::get_blog_fqn() in post_card template 2021-01-20 10:39:11 +09:00
Kitaiti Makoto be99fe5f99 Implement Post::get_blog_fqn() 2021-01-20 10:38:32 +09:00
Kitaiti Makoto 4a212e5f9e Use soft tabs instead of hard tabs in SCSS files 2021-01-17 23:14:46 +09:00
Kitaiti Makoto c253bdee54 Show edit link in post cards 2021-01-17 23:14:46 +09:00
KitaitiMakoto 46fc030df4 Merge pull request 'Search Actor' (#870) from search-actor into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/870
2021-01-17 14:13:54 +00:00
Kitaiti Makoto 02277b1ace Specify features on running migration on CI 2021-01-16 17:38:35 +09:00
Kitaiti Makoto ac507d1d42 Fix CI command 2021-01-16 16:50:57 +09:00
Kitaiti Makoto 5148349098 Add changlog about actor and searcher 2021-01-16 16:44:39 +09:00
Kitaiti Makoto e817941cc5 Update pot 2021-01-16 16:44:20 +09:00
Kitaiti Makoto 06962514fe Run database migration on CI 2021-01-16 16:39:12 +09:00
Kitaiti Makoto c3c8f6804b Remove transaction from test search::tests::search for CI 2021-01-16 16:39:12 +09:00
Kitaiti Makoto 6dd730030d Wrap Post in PostEvent with Arc
See https://github.com/riker-rs/riker/issues/153
2021-01-16 16:39:12 +09:00
Kitaiti Makoto 6c1dd6608d Add test for search actor 2021-01-16 16:39:12 +09:00
Kitaiti Makoto 42fb0cf825 Use pattern match instead of unwrap() 2021-01-16 16:39:12 +09:00
Kitaiti Makoto 2483303138 SearchActor waits for transaction commit 2021-01-16 16:39:12 +09:00
Kitaiti Makoto 47e67d9b64 Add methods to clear data of instances and users table for testing 2021-01-16 16:39:12 +09:00
Kitaiti Makoto 1d042e1bb2 Add methods to clear data of instances and users table for testing 2021-01-16 16:39:12 +09:00
Kitaiti Makoto 31418b1079 Fix search::tests::search to use searcher directly instead of actor 2021-01-16 16:39:12 +09:00
Kitaiti Makoto 68d79bfa9c Publish PostUpdated only when the post is published 2021-01-16 16:39:12 +09:00
Kitaiti Makoto a46b718937 Ignore search::tests::search 2021-01-16 16:39:12 +09:00
Kitaiti Makoto 5570494a37 Update pot 2021-01-16 16:39:12 +09:00
Kitaiti Makoto a0cd9dd6bd Fix tests to follow API changes 2021-01-16 16:39:12 +09:00
Kitaiti Makoto cd5895d560 Update pot 2021-01-16 16:39:11 +09:00
Kitaiti Makoto fc8ee1c3bc Remove searcher from arguments of Post::delete() and dependented methods 2021-01-16 16:38:18 +09:00
Kitaiti Makoto 2a8cc5f3ba Send event to channel when post deleted 2021-01-16 16:36:14 +09:00
Kitaiti Makoto 82524c9dca Update pot 2021-01-16 16:35:27 +09:00
Kitaiti Makoto 864379fb81 Remove searcher from arguments of Post::update() 2021-01-16 16:34:36 +09:00
Kitaiti Makoto aae2073146 Send event to channel when post updated 2021-01-16 16:33:29 +09:00
Kitaiti Makoto 69963689f9 Update pot 2021-01-16 16:33:29 +09:00
Kitaiti Makoto 6ad3359609 Remove searcher from arguments of Post::insert() 2021-01-16 16:25:36 +09:00
Kitaiti Makoto 6a2ba2260d Send event to channel when post published 2021-01-16 16:25:36 +09:00
Kitaiti Makoto ccbf2124cb Run cargo fmt 2021-01-16 16:22:39 +09:00
Kitaiti Makoto a733ece26e Suppress clippy 2021-01-16 16:22:39 +09:00
Kitaiti Makoto 16624fd742 Run cargo fmt 2021-01-16 16:22:39 +09:00
Kitaiti Makoto e18be6ec95 Initialize SearchActor at start 2021-01-16 16:22:39 +09:00
Kitaiti Makoto 1bfed0294f Define SearchActor::init() 2021-01-16 16:22:39 +09:00
Kitaiti Makoto d4d32bcc11 Define ACTOR_SYS and POST_CHAN 2021-01-16 16:22:39 +09:00
Kitaiti Makoto 0a148721de Define SearchActor 2021-01-16 16:22:39 +09:00
Kitaiti Makoto cfe097edf4 Define PostEvent 2021-01-16 16:22:39 +09:00
Kitaiti Makoto 578768d7df Remove unnecessary attribute to suppress clippy 2021-01-16 16:22:39 +09:00
Kitaiti Makoto 996dc309f7 Make Searcher::open_or_create() accept path and tokenizers 2021-01-16 16:22:38 +09:00
Kitaiti Makoto 09d9164a1c Extract Searcher::open_or_recreate() from main() 2021-01-16 16:22:38 +09:00
Kitaiti Makoto 43f32d30cb Add once_cell to dependdencies 2021-01-16 16:22:38 +09:00
Kitaiti Makoto b1d7b3969d Add Riker to dependencies 2021-01-16 16:20:49 +09:00
KitaitiMakoto 89fa208bec Update CHANGELOG (#881)
Update CHANGELOG

Co-authored-by: Kitaiti Makoto <KitaitiMakoto@gmail.com>
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/881
2021-01-16 05:49:12 +00:00
KitaitiMakoto a11205324b Merge pull request 'Upgrade Tantivy to 0.13.3' (#878) from upgrade-tantivy into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/878
2021-01-16 05:40:14 +00:00
Kitaiti Makoto 7d17b12000 Upgrade lindera-tantivy to 0.7.1 2021-01-16 13:34:15 +09:00
Kitaiti Makoto 885dcf91fe Remove redundant imports 2021-01-16 12:53:05 +09:00
Kitaiti Makoto 192c0f8efd Remove unused lifetime 2021-01-16 12:21:49 +09:00
Kitaiti Makoto 35e71b0f8a Replace string range access with strip_prefix() 2021-01-16 12:20:21 +09:00
Kitaiti Makoto 1a24ba44b2 Remove redundant imports 2021-01-16 12:12:13 +09:00
Kitaiti Makoto 7c00d4a88a Remove no-landing-pads option from CI environment 2021-01-16 12:03:56 +09:00
Kitaiti Makoto 476310a217 Update plume.pot 2021-01-16 01:15:48 +09:00
Kitaiti Makoto 26ffde726f Run cargo fmt 2021-01-16 01:13:45 +09:00
Kitaiti Makoto 3fd89e6b48 Remove unnecessary wraps 2021-01-16 01:05:42 +09:00
Kitaiti Makoto be939cf169 Suppress clippy 2021-01-16 00:59:07 +09:00
Kitaiti Makoto 4a15bef34f Impl From<ListType> for i32 instead of Into<i32> for ListType 2021-01-16 00:49:34 +09:00
Kitaiti Makoto 6eee4b0b65 Define custom errors 2021-01-16 00:43:41 +09:00
Kitaiti Makoto f720dcbe9a Remove underscore prefix from route methods to suppress clippy 2021-01-15 23:58:48 +09:00
Kitaiti Makoto 33fceddc08 Use is_empty() instead of comparing with "" 2021-01-15 23:25:07 +09:00
Kitaiti Makoto dfe83aafc5 Replace as_ref().map() with as_deref() 2021-01-15 23:21:33 +09:00
Kitaiti Makoto 0775c8f3f9 Fix redundant method chains 2021-01-15 23:19:47 +09:00
Kitaiti Makoto 4cdc1a3655 Use char instead of one length &str 2021-01-15 23:01:37 +09:00
Kitaiti Makoto bbd415c392 Use strip_prefix() instead of range access 2021-01-15 23:00:15 +09:00
Kitaiti Makoto a236f33269 Remove unused macro_use 2021-01-15 22:38:40 +09:00
Kitaiti Makoto 9b593c76e4 Remove unused doc comments 2021-01-15 22:36:59 +09:00
Kitaiti Makoto 012c8d2026 Remove redundant imports 2021-01-15 22:35:19 +09:00
Kitaiti Makoto cb05571339 Upgrade tantivy to 0.13.3 2021-01-15 10:19:54 +09:00
Kitaiti Makoto 64e4550dea Update rust-toolchain to nightly-2021-01-15 2021-01-15 10:19:12 +09:00
KitaitiMakoto ec8c17482d Merge pull request 'proxy support' (#829) from dr-bonez/Plume:feature/proxy into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/829
Reviewed-by: KitaitiMakoto <kitaitimakoto@noreply@joinplu.me>
2021-01-13 14:20:36 +00:00
Aiden McClelland f167aad0fb fix doc tests 2021-01-11 13:29:01 -07:00
Aiden McClelland 24aac22b89 proxy support 2021-01-11 13:27:52 -07:00
KitaitiMakoto 615a5fe346 Merge pull request 'Update CHANGELOG' (#877) from changelog into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/877
2021-01-11 18:15:58 +00:00
Kitaiti Makoto a6699a7319 Add about translation languages to CHANGELOG 2021-01-12 03:12:59 +09:00
Kitaiti Makoto 0f408bc738 Update CHANGELOG 2021-01-12 03:11:29 +09:00
KitaitiMakoto 44a7953c20 Merge pull request 'GitHub 838: Fix menu animation not opening on iOS' (#876) from ios_nav_menu_animation_fixed-4 into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/876
2021-01-11 17:53:30 +00:00
Marek Ľach 21069ea18e Fix code formatting 2021-01-11 18:04:21 +01:00
Marek Ľach 1e1fd49c8c Update _header.scss 2021-01-11 17:48:39 +01:00
Marek Ľach 84621d1d27 Update _header.scss 2021-01-11 01:40:17 +01:00
Marek Ľach ba4476bf5a Update _header.scss 2021-01-11 01:34:09 +01:00
KITAITI Makoto f87bc455b7 Merge branch 'main' into ios_nav_menu_animation_fixed-4 2021-01-11 09:26:37 +09:00
KitaitiMakoto 76ec0d2b13 Merge pull request 'Initialize tracing subscriber' (#875) from init-tracing into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/875
2021-01-11 00:24:54 +00:00
Kitaiti Makoto 4d547082e0 Update pot 2021-01-11 09:15:45 +09:00
Kitaiti Makoto b9b9af6c44 Initialize tracing subscriber 2021-01-11 09:15:34 +09:00
Kitaiti Makoto 4a4534d6f3 Run cargo fmt 2021-01-11 09:05:31 +09:00
Kitaiti Makoto 894c4fee2c Add tracing-subscriber to dependencies 2021-01-11 09:05:28 +09:00
Kitaiti Makoto 27a33c833d Run cargo fmt 2021-01-11 08:38:41 +09:00
Marek Ľach b52d8e4e26 Fix menu animation not working on mobile iOS (also Midori) 2021-01-10 21:23:11 +01:00
Marek Ľach b059fb48fc Update _header.scss
The menu does not open on iOS Safari because of how it can't handle translateZ, and needs an -webkit specific prefix for the @keyframes property for the animation to work. This PR fixes the issue.
2021-01-10 18:32:21 +01:00
KitaitiMakoto f33b057961 Merge pull request 'Add SQlite journal files to .gitignore' (#874) from ignore-journal into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/874
2021-01-10 09:08:36 +00:00
Kitaiti Makoto 663ca4f530 Add SQlite journal files to .gitignore 2021-01-10 18:05:17 +09:00
KitaitiMakoto 5892c0b088 Merge pull request 'Remove SQLite journal files' (#873) from sqlite-journal into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/873
2021-01-10 06:31:14 +00:00
Kitaiti Makoto 044a24eac8 Remove SQLite journal files 2021-01-10 10:27:53 +09:00
Kitaiti Makoto b93ebedaf3 Revert "Initialize Rocket early to initialize logger early"
This reverts commit 30093d3a9f.
2021-01-09 18:41:47 +09:00
KitaitiMakoto 8539bdaf72 Merge pull request 'Fix #623 tracing crate' (#868) from logging into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/868

I read tracing's documentation and thought that is what I need. I decided use it instead of log crate. Thank you for info, igalic.
2021-01-06 13:38:48 +00:00
Kitaiti Makoto c1abd3d6f9 Use eprintln! instead of println! for console message 2021-01-06 21:40:14 +09:00
Kitaiti Makoto becb40544c Replace println!()s with logging macros 2021-01-06 21:40:14 +09:00
Kitaiti Makoto ee96d2b754 Fix a typo 2021-01-06 21:40:14 +09:00
Kitaiti Makoto 30093d3a9f Initialize Rocket early to initialize logger early 2021-01-06 21:40:14 +09:00
Kitaiti Makoto 29f2239e3f Extract dotenv initialization from init_pool because it affects not only database 2021-01-06 21:40:14 +09:00
Kitaiti Makoto 7f1b33a567 Add tracing crate to dependences 2021-01-06 21:40:07 +09:00
KitaitiMakoto d0dd23a1af Merge pull request 'Percent encode URI for remote_interact' (#866) from encode-remote-interact into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/866
2021-01-02 21:28:27 +00:00
Kitaiti Makoto f8774223d4 Update pot file 2021-01-03 05:51:38 +09:00
Kitaiti Makoto bca870f1f8 Percent encode URI for remote_interact 2021-01-03 05:49:45 +09:00
KitaitiMakoto 64d38c88e8 Merge pull request 'Add scripts to make prebuilt binaries' (#864) from prebuild-script into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/864
2021-01-02 16:46:02 +00:00
Kitaiti Makoto 3ac94b3e43 Add script to prebuild in Docker container 2021-01-02 23:51:09 +09:00
Kitaiti Makoto 254eef6a2a Add prebuild script 2021-01-02 23:48:30 +09:00
KitaitiMakoto f1255f477f Merge pull request 'Add translated languages to plume-front' (#863) from add-langs-front into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/863
2021-01-02 14:36:22 +00:00
Kitaiti Makoto 6c7b95c1b9 Add translated languages to plume-front 2021-01-02 23:29:46 +09:00
KitaitiMakoto e7d17caf9d Merge pull request 'Update crates' (#858) from update-crates into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/858
2021-01-02 14:23:45 +00:00
KitaitiMakoto 71dd80f125 Merge pull request 'Add translated languages' (#862) from add-langs into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/862
2021-01-02 14:21:25 +00:00
KitaitiMakoto ed4761e841 Merge pull request 'fix-test' (#861) from fix-test into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/861
2021-01-02 14:20:49 +00:00
Kitaiti Makoto fe05f9a58e cargo fmt 2021-01-02 22:54:46 +09:00
Kitaiti Makoto 51e5a401a5 Update Welsh translation 2021-01-02 22:45:04 +09:00
Kitaiti Makoto f1990b782d Fix tests 2021-01-02 22:15:26 +09:00
KitaitiMakoto 4e3ed0f4c1 Merge pull request 'add dir to allowed attributes' (#860) from dir-safestring into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/860
Reviewed-by: Mina Galić <me+git@igalic.co>
Reviewed-by: KitaitiMakoto <kitaitimakoto@noreply@joinplu.me>
2021-01-02 10:46:32 +00:00
Trinity Pointard fdb58215c0 add dir to allowed attributes 2021-01-02 00:26:56 +01:00
Kitaiti Makoto 47c8711933 Run cargo fmt 2021-01-02 07:10:10 +09:00
Kitaiti Makoto 20a099c16f Replace Syntect's deprecated APIs with new APIs 2021-01-02 06:47:55 +09:00
Kitaiti Makoto 3824e4ba25 Upgrade shrinkwraprs 2021-01-02 06:04:59 +09:00
Kitaiti Makoto b163fc8423 Remove unused serde_qs crate 2021-01-02 04:32:41 +09:00
Kitaiti Makoto b50f723a4d Upgrade Ructe 2021-01-02 04:24:06 +09:00
Kitaiti Makoto b778933c64 Upgrade rpassword 2021-01-02 04:04:17 +09:00
Kitaiti Makoto 12b97ce713 Remove unused colored crate 2021-01-02 03:16:22 +09:00
Kitaiti Makoto 4025a45d52 Upgrade whatlang 2020-12-30 22:36:47 +09:00
Kitaiti Makoto 8a6d832513 Upgrade rocket 2020-12-30 22:35:24 +09:00
Kitaiti Makoto 0b0b3a158f Upgrade diesel 2020-12-30 22:31:02 +09:00
Kitaiti Makoto 6b745ca8f0 Upgrade dotenv 2020-12-30 22:28:34 +09:00
Kitaiti Makoto 1f0cb7e05c Uninstall syntect 2020-12-30 21:27:15 +09:00
Kitaiti Makoto c59821945d Remove dependency to syntect from root package 2020-12-30 21:26:41 +09:00
Kitaiti Makoto 1a723b78b7 Install syntect 2020-12-30 21:23:52 +09:00
Kitaiti Makoto e7ff7e3263 Upgrade syntect to 0.4.5 2020-12-30 21:23:41 +09:00
KitaitiMakoto 3a6c1aeeef Merge pull request 'Bump version to development version' (#856) from dev-version into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/856
2020-12-29 14:30:43 +00:00
Kitaiti Makoto 567e011a66 Update dependencies 2020-12-29 22:52:11 +09:00
Kitaiti Makoto eaf63e0b17 start next development iteration 2020-12-29 18:01:18 +09:00
Kitaiti Makoto 63b31a9154 Fix changelog about security fix scope 2020-12-29 17:58:24 +09:00
204 changed files with 15766 additions and 10581 deletions
+10
View File
@@ -0,0 +1,10 @@
[target.wasm32-unknown-unknown]
# required for clippy
rustflags = [
"--cfg", "web_sys_unstable_apis",
]
[target.x86_64-unknown-linux-gnu]
rustflags = [
"--cfg", "web_sys_unstable_apis",
]
+14 -10
View File
@@ -10,7 +10,7 @@ executors:
type: boolean
default: false
docker:
- image: plumeorg/plume-buildenv:v0.3.0
- image: plumeorg/plume-buildenv:v0.4.0
- image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
environment:
POSTGRES_USER: postgres
@@ -21,6 +21,7 @@ executors:
RUST_TEST_THREADS: 1
FEATURES: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
DATABASE_URL: <<#parameters.postgres>>postgres://postgres@localhost/plume<</parameters.postgres>><<^parameters.postgres>>plume.sqlite<</parameters.postgres>>
ROCKET_SECRET_KEY: VN5xV1DN7XdpATadOCYcuGeR/dV0hHfgx9mx9TarLdM=
commands:
@@ -62,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
@@ -71,7 +72,7 @@ commands:
type: string
steps:
- run: |
export RUSTFLAGS="-Zprofile -Zfewer-names -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zno-landing-pads -Clink-arg=-Xlinker -Clink-arg=--no-keep-memory -Clink-arg=-Xlinker -Clink-arg=--reduce-memory-overheads"
export RUSTFLAGS="-Zprofile -Zfewer-names -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Clink-arg=-Xlinker -Clink-arg=--no-keep-memory -Clink-arg=-Xlinker -Clink-arg=--reduce-memory-overheads"
export CARGO_INCREMENTAL=0
<< parameters.cmd >>
@@ -99,7 +100,7 @@ commands:
steps:
- run: |
cmd="cargo build <<#parameters.release>>--release<</parameters.release>> --no-default-features --features="${FEATURES}" -p <<parameters.package>> -j"
for i in 36 4 2 1 1; do
for i in 16 4 2 1 1; do
$cmd $i && exit 0
done
exit 1
@@ -143,11 +144,14 @@ jobs:
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
- run_with_coverage:
cmd: |
cargo build -p plume-cli --no-default-features --features=${FEATURES} -j 4
./target/debug/plm migration run
./target/debug/plm search init
cmd="cargo test --all --exclude plume-front --exclude plume-macro --no-run --no-default-features --features=${FEATURES} -j"
for i in 36 4 2 1 1; do
for i in 16 4 2 1 1; do
$cmd $i && break
done
cargo test --all --exclude plume-front --exclude plume-macro --no-default-features --features="${FEATURES}" -j1 -- --test-threads=1
cargo test --all --exclude plume-front --exclude plume-macro --no-default-features --features="${FEATURES}" -j1
- upload_coverage:
type: unit
- cache:
@@ -164,18 +168,18 @@ jobs:
steps:
- restore_env:
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
- run: cargo web deploy -p plume-front
- run: wasm-pack build --target web --release plume-front
- run_with_coverage:
cmd: |
cmd="cargo install --debug --no-default-features --features="${FEATURES}",test --force --path . -j"
for i in 36 4 2 1 1; do
for i in 16 4 2 1 1; do
$cmd $i && exit 0
done
exit 1
- run_with_coverage:
cmd: |
cmd="cargo install --debug --no-default-features --features="${FEATURES}" --force --path plume-cli -j"
for i in 36 4 2 1 1; do
for i in 16 4 2 1 1; do
$cmd $i && exit 0
done
exit 1
@@ -199,7 +203,7 @@ jobs:
steps:
- restore_env:
cache: release-<<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
- run: cargo web deploy -p plume-front --release
- run: wasm-pack build --target web --release plume-front
- build:
package: plume
release: true
+3 -3
View File
@@ -1,4 +1,4 @@
FROM debian:buster-20201117
FROM debian:buster-20210208
ENV PATH="/root/.cargo/bin:${PATH}"
#install native/circleci/build dependancies
@@ -11,12 +11,12 @@ 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-2020-12-07 -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
#compile some deps
RUN cargo install cargo-web &&\
RUN cargo install wasm-pack &&\
cargo install grcov &&\
strip /root/.cargo/bin/* &&\
rm -fr ~/.cargo/registry
+3
View File
@@ -15,6 +15,9 @@ DATABASE_URL=postgres://plume:plume@localhost/plume
# The domain of your instance
BASE_URL=plu.me
# Log level for each crate
RUST_LOG=info
# The secret key for private cookies and CSRF protection
# You can generate one with `openssl rand -base64 32`
ROCKET_SECRET_KEY=
@@ -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 }}
+1
View File
@@ -19,3 +19,4 @@ search_index
.buildconfig
__pycache__
.vscode/
*-journal
+57 -2
View File
@@ -4,6 +4,59 @@
## [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)
- More translation languages (#862)
- 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-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
- Percent-encode URI for remote_interact (#866, #857)
- Menu animation not opening on iOS (#876, #897)
- Make actors subscribe to channel once (#913)
- Upsert posts and media instead of trying to insert and fail (#912)
- Update post's ActivityPub id when published by update (#915)
- Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured (#916)
- Prevent duplicated posts in 'all' timeline (#917)
- Draw side line for blockquote on start (#933)
- Fix URIs of posts on Mastodon (#947)
- Place edit link proper position (#956, #963, #964)
## [[0.6.0]] - 2020-12-29
### Added
@@ -34,7 +87,7 @@
### Security
- Validate spoofing of Create activity
- Validate spoofing of activity
## [0.5.0] - 2020-06-21
@@ -183,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
+2838 -2346
View File
File diff suppressed because it is too large Load Diff
+14 -19
View File
@@ -1,38 +1,33 @@
[package]
authors = ["Plume contributors"]
name = "plume"
version = "0.6.0"
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"
colored = "1.8"
dotenv = "0.14"
dotenv = "0.15.0"
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
guid-create = "0.1"
heck = "0.3.0"
lettre = "0.9.2"
guid-create = "0.2"
lettre_email = "0.9.2"
num_cpus = "1.10"
rocket = "0.4.5"
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"
serde_qs = "0.5"
shrinkwraprs = "0.2.1"
syntect = "3.3"
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.3.5"
riker = "0.4.2"
[[bin]]
name = "plume"
@@ -48,12 +43,12 @@ version = "3.1.2"
[dependencies.diesel]
features = ["r2d2", "chrono"]
version = "*"
version = "1.4.5"
[dependencies.multipart]
default-features = false
features = ["server"]
version = "0.16"
version = "0.18"
[dependencies.plume-api]
path = "plume-api"
@@ -69,8 +64,8 @@ git = "https://github.com/fdb-hiroshima/rocket_csrf"
rev = "29910f2829e7e590a540da3804336577b48c7b31"
[build-dependencies]
ructe = "0.9.0"
rsass = "0.9"
ructe = "0.13.0"
rsass = "0.23"
[features]
default = ["postgres"]
+1 -1
View File
@@ -19,7 +19,7 @@ RUN chmod a+x ./wasm-deps.sh && sleep 1 && ./wasm-deps.sh
WORKDIR /app
COPY Cargo.toml Cargo.lock rust-toolchain ./
RUN cargo install cargo-web
RUN cargo install wasm-pack
COPY . .
+1 -1
View File
@@ -20,7 +20,7 @@ RUN chmod a+x ./wasm-deps.sh && sleep 1 && ./wasm-deps.sh
WORKDIR /app
COPY Cargo.toml Cargo.lock rust-toolchain ./
RUN cargo install diesel_cli --no-default-features --features postgres --version '=1.3.0'
RUN cargo install cargo-web
RUN cargo install wasm-pack
COPY . .
+1 -1
View File
@@ -30,7 +30,7 @@ A lot of features are still missing, but what is already here should be quite st
- **Media management**: you can upload pictures to illustrate your articles, but also audio files if you host a podcast, and manage them all from Plume.
- **Federation**: Plume is part of a network of interconnected websites called the Fediverse. Each of these websites (often called *instances*) have their own
rules and thematics, but they can all communicate with each other.
- **Collaborative writing**: invite other people to your blogs, and write articles together.
- **Collaborative writing**: invite other people to your blogs, and write articles together. (Not implemented yet, but will be in 1.0)
## Get involved
+1 -1
View File
@@ -1,3 +1,3 @@
* {
font-family: monospace;
font-family: monospace;
}
+96 -89
View File
@@ -64,41 +64,41 @@ main header.article {
}
main .article-info {
margin: 0 auto 3em;
font-size: 0.95em;
font-weight: 400;
margin: 0 auto 3em;
font-size: 0.95em;
font-weight: 400;
.author, .author a {
font-weight: 600;
}
.author, .author a {
font-weight: 600;
}
}
/* The article itself */
main article {
max-width: $article-width;
margin: 2.5em auto;
font-family: $lora;
font-size: 1.2em;
line-height: 1.7;
margin: 2.5em auto;
font-family: $lora;
font-size: 1.2em;
line-height: 1.7;
a:hover {
text-decoration: underline;
}
a:hover {
text-decoration: underline;
}
img {
display: block;
margin: 3em auto;
max-width: 100%;
img {
display: block;
margin: 3em auto;
max-width: 100%;
}
pre {
padding: 1em;
background: $gray;
overflow: auto;
padding: 1em;
background: $gray;
overflow: auto;
}
blockquote {
border-left: 5px solid $gray;
border-inline-start: 5px solid $gray;
margin: 1em auto;
padding: 0em 2em;
}
@@ -126,7 +126,7 @@ main .article-meta {
> p {
margin: 2em $horizontal-margin;
font-size: 0.9em;
font-size: 0.9em;
}
/* Article Tags */
@@ -157,15 +157,15 @@ main .article-meta {
/* Likes & Boosts */
.actions {
display: flex;
flex-direction: row;
justify-content: space-around;
flex-direction: row;
justify-content: space-around;
}
.likes, .reshares {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5em 0;
flex-direction: column;
align-items: center;
padding: 0.5em 0;
p {
font-size: 1.5em;
@@ -175,34 +175,34 @@ main .article-meta {
.action {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
background: none;
color: $text-color;
border: none;
font-size: 1.1em;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
background: none;
color: $text-color;
border: none;
font-size: 1.1em;
cursor: pointer;
svg.feather {
transition: background 0.1s ease-in;
display: flex;
align-items: center;
justify-content: center;
svg.feather {
transition: background 0.1s ease-in;
display: flex;
align-items: center;
justify-content: center;
margin: 0.5em 0;
width: 2.5em;
height: 2.5em;
margin: 0.5em 0;
width: 2.5em;
height: 2.5em;
border-radius: 50%;
border-radius: 50%;
}
&.reshared, &.liked {
svg.feather {
color: $background;
font-weight: 900;
color: $background;
font-weight: 900;
}
}
}
@@ -213,14 +213,14 @@ main .article-meta {
.action svg.feather {
padding: 0.7em;
box-sizing: border-box;
color: $red;
fill: none;
border: solid $red thin;
box-sizing: border-box;
color: $red;
fill: none;
border: solid $red thin;
}
.action:hover svg.feather {
background: transparentize($red, 0.85);
background: transparentize($red, 0.85);
}
.action.liked svg.feather {
@@ -238,22 +238,22 @@ main .article-meta {
.action svg.feather {
padding: 0.7em;
box-sizing: border-box;
color: $primary;
border: solid $primary thin;
font-weight: 600;
box-sizing: border-box;
color: $primary;
border: solid $primary thin;
font-weight: 600;
}
.action:hover svg.feather {
background: transparentize($primary, 0.85);
background: transparentize($primary, 0.85);
}
.action.reshared svg.feather {
background: $primary;
}
.action.reshared:hover svg.feather {
background: transparentize($primary, 0.75)
color: $primary;
background: transparentize($primary, 0.75)
color: $primary;
}
}
@@ -262,9 +262,9 @@ main .article-meta {
margin: 0 $horizontal-margin;
h2 {
color: $primary;
font-size: 1.5em;
font-weight: 600;
color: $primary;
font-size: 1.5em;
font-weight: 600;
}
summary {
@@ -279,16 +279,16 @@ main .article-meta {
// Respond & delete comment buttons
a.button, form.inline, form.inline input {
padding: 0;
background: none;
color: $text-color;
margin-right: 2em;
font-family: $route159;
padding: 0;
background: none;
color: $text-color;
margin-right: 2em;
font-family: $route159;
font-weight: normal;
&::before {
color: $primary;
padding-right: 0.5em;
&::before {
color: $primary;
padding-right: 0.5em;
}
&:hover { color: $primary; }
@@ -296,8 +296,8 @@ main .article-meta {
.comment {
margin: 1em 0;
font-size: 1em;
border: none;
font-size: 1em;
border: none;
.content {
background: $gray;
@@ -328,36 +328,36 @@ main .article-meta {
color: transparentize($text-color, 0.6);
}
.author {
display: flex;
flex-direction: row;
align-items: center;
align-content: center;
.author {
display: flex;
flex-direction: row;
align-items: center;
align-content: center;
* {
transition: all 0.1s ease-in;
}
* {
transition: all 0.1s ease-in;
}
.display-name {
color: $text-color;
.display-name {
color: $text-color;
}
&:hover {
.display-name { color: $primary; }
small { opacity: 1; }
}
}
}
& > .comment {
padding-left: 2em;
}
.text {
padding: 1.25em 0;
font-family: $lora;
font-size: 1.1em;
line-height: 1.4;
text-align: left;
.text {
padding: 1.25em 0;
font-family: $lora;
font-size: 1.1em;
line-height: 1.4;
text-align: left;
}
}
}
@@ -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;
}
}
}
+48 -48
View File
@@ -1,27 +1,27 @@
label {
display: block;
margin: 2em auto .5em;
font-size: 1.2em;
display: block;
margin: 2em auto .5em;
font-size: 1.2em;
}
input, textarea, select {
transition: all 0.1s ease-in;
display: block;
width: 100%;
margin: auto;
padding: 1em;
box-sizing: border-box;
transition: all 0.1s ease-in;
display: block;
width: 100%;
margin: auto;
padding: 1em;
box-sizing: border-box;
-webkit-appearance: textarea;
background: $form-input-background;
color: $text-color;
border: solid $form-input-border thin;
background: $form-input-background;
color: $text-color;
border: solid $form-input-border thin;
font-size: 1.2em;
font-weight: 400;
font-size: 1.2em;
font-weight: 400;
&:focus {
border-color: $primary;
}
&:focus {
border-color: $primary;
}
}
form input[type="submit"] {
margin: 2em auto;
@@ -29,18 +29,18 @@ form input[type="submit"] {
}
textarea {
resize: vertical;
resize: vertical;
overflow-y: scroll;
font-family: $lora;
font-size: 1.1em;
line-height: 1.5;
font-family: $lora;
font-size: 1.1em;
line-height: 1.5;
}
input[type="checkbox"] {
display: inline;
margin: initial;
min-width: initial;
width: initial;
display: inline;
margin: initial;
min-width: initial;
width: initial;
-webkit-appearance: checkbox;
}
@@ -71,31 +71,31 @@ form.inline {
}
.button, .button:visited, input[type="submit"], input[type="submit"].button {
transition: all 0.1s ease-in;
display: inline-block;
transition: all 0.1s ease-in;
display: inline-block;
-webkit-appearance: none;
margin: 0.5em auto;
padding: 0.75em 1em;
margin: 0.5em auto;
padding: 0.75em 1em;
background: $primary;
color: $primary-text-color;
background: $primary;
color: $primary-text-color;
font-weight: bold;
border: none;
cursor: pointer;
cursor: pointer;
&:hover {
background: transparentize($primary, 0.1);
}
&:hover {
background: transparentize($primary, 0.1);
}
&.destructive {
background: $red;
&.destructive {
background: $red;
&:hover {
background: transparentize($red, 0.1);
}
}
}
&.secondary {
background: $gray;
@@ -115,20 +115,20 @@ input[type="submit"] {
form.new-post {
max-width: 60em;
.title {
margin: 0 auto;
padding: 0.75em 0;
margin: 0 auto;
padding: 0.75em 0;
background: none;
border: none;
background: none;
border: none;
font-family: $playfair;
font-size: 2em;
text-align: left;
font-family: $playfair;
font-size: 2em;
text-align: left;
}
textarea {
min-height: 20em;
overflow-y: scroll;
resize: none;
min-height: 20em;
overflow-y: scroll;
resize: none;
-webkit-appearance: textarea;
}
}
+121 -96
View File
@@ -6,43 +6,43 @@ html {
}
html, body {
margin: 0;
padding: 0;
background: $background;
color: $text-color;
font-family: $route159;
margin: 0;
padding: 0;
background: $background;
color: $text-color;
font-family: $route159;
::selection {
background: transparentize($primary, 0.7);
}
::-moz-selection {
::-moz-selection {
background: transparentize($primary, 0.7);
}
}
}
a, a:visited {
color: $primary;
text-decoration: none;
color: $primary;
text-decoration: none;
}
a::selection {
color: $background;
color: $background;
}
a::-moz-selection {
color: $background;
color: $background;
}
small {
margin-left: 1em;
color: transparentize($text-color, 0.6);
font-size: 0.75em;
word-wrap: break-word;
word-break: break-all;
margin-left: 1em;
color: transparentize($text-color, 0.6);
font-size: 0.75em;
word-wrap: break-word;
word-break: break-all;
}
.center {
text-align: center;
font-weight: bold;
opacity: 0.6;
padding: 5em;
text-align: center;
font-weight: bold;
opacity: 0.6;
padding: 5em;
}
.right {
@@ -53,28 +53,28 @@ small {
}
.spaced {
margin: 4rem 0;
margin: 4rem 0;
}
.banner {
background: $gray;
padding-top: 2em;
padding-bottom: 1em;
margin: 3em 0px;
background: $gray;
padding-top: 2em;
padding-bottom: 1em;
margin: 3em 0px;
}
.hidden {
display: none;
appearance: none;
display: none;
appearance: none;
}
/* Main */
body > main > *, .h-feed > * {
margin: 1em $horizontal-margin;
margin: 1em $horizontal-margin;
}
body > main > .h-entry, .h-feed {
margin: 0;
margin: 0;
}
body > main {
@@ -98,18 +98,18 @@ main {
margin-top: 1em;
&.article {
margin: 1em auto 0.5em;
font-family: $playfair;
font-size: 2.5em;
font-weight: normal;
margin: 1em auto 0.5em;
font-family: $playfair;
font-size: 2.5em;
font-weight: normal;
}
}
h2 {
font-size: 1.75em;
font-weight: 300;
font-size: 1.75em;
font-weight: 300;
&.article {
&.article {
font-size: 1.25em;
margin-bottom: 0.5em;
}
@@ -139,15 +139,15 @@ main {
/* Errors */
p.error {
color: $red;
font-weight: bold;
color: $red;
font-weight: bold;
}
/* User page */
.user h1 {
display: flex;
flex-direction: row;
align-items: center;
display: flex;
flex-direction: row;
align-items: center;
margin: 0px;
}
@@ -156,14 +156,14 @@ p.error {
}
.badge {
margin-right: 1em;
padding: 0.35em 1em;
margin-right: 1em;
padding: 0.35em 1em;
background: $background;
color: $primary;
border: 1px solid $primary;
background: $background;
color: $primary;
border: 1px solid $primary;
font-size: 1rem;
font-size: 1rem;
}
.user-summary {
@@ -172,23 +172,25 @@ p.error {
/* Cards */
.cards {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: 0 5%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: 0 5%;
margin: 1rem 0 5rem;
}
.card {
flex: 1;
display: flex;
flex-direction: column;
flex: 1;
display: flex;
flex-direction: column;
min-width: 20em;
min-height: 20em;
margin: 1em;
box-sizing: border-box;
position: relative;
background: $gray;
min-width: 20em;
min-height: 20em;
margin: 1em;
box-sizing: border-box;
background: $gray;
text-overflow: ellipsis;
@@ -213,38 +215,68 @@ p.error {
}
> * {
margin: 20px;
}
> * {
margin: 20px;
}
.cover {
.cover-link {
margin: 0;
&:hover {
opacity: 0.9;
}
}
.cover {
min-height: 10em;
background-position: center;
background-size: cover;
margin: 0px;
}
h3 {
margin: 0.75em 20px;
font-family: $playfair;
font-size: 1.75em;
font-weight: normal;
a {
transition: color 0.1s ease-in;
color: $text-color;
header {
display: flex;
}
&:hover { color: $primary; }
}
h3 {
flex-grow: 1;
margin: 0;
font-family: $playfair;
font-size: 1.75em;
font-weight: normal;
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;
&:hover { color: $primary; }
}
}
.controls {
flex-shrink: 0;
text-align: end;
.button {
margin-top: 0;
margin-bottom: 0;
}
}
main {
flex: 1;
flex: 1;
font-family: $lora;
font-size: 1em;
line-height: 1.25;
text-align: left;
overflow: hidden;
font-family: $lora;
font-size: 1em;
line-height: 1.25;
text-align: initial;
overflow: hidden;
}
}
@@ -286,15 +318,15 @@ p.error {
/* Stats */
.stats {
display: flex;
justify-content: space-around;
margin: 2em;
display: flex;
justify-content: space-around;
margin: 2em;
> div {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> div {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
p {
@@ -447,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;
@@ -543,14 +576,6 @@ figure {
}
}
.bottom-bar {
flex-direction: column;
align-items: center;
& > div {
margin: 0;
}
}
main .article-meta .comments .comment {
header {
flex-direction: column;
+84 -61
View File
@@ -3,8 +3,8 @@ body > header {
#content {
display: flex;
align-content: center;
justify-content: space-between;
align-content: center;
justify-content: space-between;
}
nav#menu {
@@ -19,44 +19,44 @@ body > header {
a {
transform: skewX(15deg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 1.4em;
height: 1.4em;
margin: 0;
padding: 0;
color: $gray;
font-size: 1.33em;
}
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 1.4em;
height: 1.4em;
margin: 0;
padding: 0;
color: $gray;
font-size: 1.33em;
}
}
nav {
display: flex;
flex-direction: row;
align-items: center;
flex-direction: row;
align-items: center;
hr {
height: 100%;
width: 0.2em;
background: $primary;
border: none;
transform: skewX(-15deg);
hr {
height: 100%;
width: 0.2em;
background: $primary;
border: none;
transform: skewX(-15deg);
}
a {
display: flex;
align-items: center;
position: relative;
align-self: stretch;
margin: 0;
padding: 0 2em;
font-size: 1em;
display: flex;
align-items: center;
position: relative;
align-self: stretch;
margin: 0;
padding: 0 2em;
font-size: 1em;
i { font-size: 1.2em; }
i { font-size: 1.2em; }
&.title {
margin: 0;
&.title {
margin: 0;
text-align: center;
padding: 0.5em 1em;
font-size: 1.75em;
@@ -70,7 +70,7 @@ body > header {
margin: 0;
padding-left: 0.5em;
}
}
}
}
}
}
@@ -115,6 +115,18 @@ body > header {
opacity: 1;
}
}
@-webkit-keyframes menuOpening {
from {
-webkit-transform: scaleX(0);
transform-origin: left;
opacity: 0;
}
to {
-webkit-transform: scaleX(1);
transform-origin: left;
opacity: 1;
}
}
body > header {
flex-direction: column;
@@ -132,7 +144,7 @@ body > header {
}
}
body > header:focus-within #content, #content.show {
body > header:focus-within #content, .show + #content {
position: fixed;
display: flex;
flex-direction: column;
@@ -193,34 +205,33 @@ body > header {
/* Only enable label animations on large screens */
@media screen and (min-width: 600px) {
header nav a {
i {
transition: all 0.2s ease;
margin: 0;
}
.mobile-label {
transition: all 0.2s ease;
display: block;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
transform: translateZ(0);
opacity: 0;
font-size: 0.9em;
white-space: nowrap;
}
img + .mobile-label { display: none; }
&:hover {
i { margin-bottom: 0.75em; }
.mobile-label {
opacity: 1;
transform: translate(-50%, 80%);
}
header nav a {
i {
transition: all 0.2s ease;
margin: 0;
}
}
.mobile-label {
transition: all 0.2s ease;
display: block;
position: absolute;
left: 50%;
transform: translateZ(0);
opacity: 0;
font-size: 0.9em;
white-space: nowrap;
}
img + .mobile-label { display: none; }
&:hover {
i { margin-bottom: 0.75em; }
.mobile-label {
opacity: 1;
transform: translate(-50%, 80%);
}
}
}
}
// Small screens
@@ -237,6 +248,18 @@ body > header {
opacity: 1;
}
}
@-webkit-keyframes menuOpening {
from {
-webkit-transform: scaleX(0);
transform-origin: left;
opacity: 0;
}
to {
-webkit-transform: scaleX(1);
transform-origin: left;
opacity: 1;
}
}
body > header {
flex-direction: column;
@@ -254,7 +277,7 @@ body > header {
}
}
body > header:focus-within #content, #content.show {
body > header:focus-within #content, .show + #content {
position: fixed;
display: flex;
flex-direction: column;
+20 -22
View File
@@ -1,5 +1,3 @@
use rsass;
use ructe::Ructe;
use std::process::{Command, Stdio};
use std::{ffi::OsStr, fs::*, io::Write, path::*};
@@ -43,26 +41,20 @@ 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");
let cache_id = &compute_static_hash()[..8];
println!("cargo:rerun-if-changed=target/deploy/plume-front.wasm");
copy("target/deploy/plume-front.wasm", "static/plume-front.wasm")
.and_then(|_| read_to_string("target/deploy/plume-front.js"))
.and_then(|js| {
write(
"static/plume-front.js",
js.replace(
"\"plume-front.wasm\"",
&format!("\"/static/cached/{}/plume-front.wasm\"", cache_id),
),
)
})
.ok();
println!("cargo:rerun-if-changed=plume-front/pkg/plume_front_bg.wasm");
copy(
"plume-front/pkg/plume_front_bg.wasm",
"static/plume_front_bg.wasm",
)
.and_then(|_| copy("plume-front/pkg/plume_front.js", "static/plume_front.js"))
.ok();
println!("cargo:rustc-env=CACHE_ID={}", cache_id)
}
@@ -105,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("-");
@@ -128,8 +120,14 @@ fn compile_theme(path: &Path, out_dir: &Path) -> std::io::Result<()> {
// compile the .scss/.sass file
let mut out = File::create(out.join("theme.css"))?;
out.write_all(
&rsass::compile_scss_file(path, rsass::OutputStyle::Compressed)
.expect("SCSS compilation error"),
&rsass::compile_scss_path(
path,
rsass::output::Format {
style: rsass::output::Style::Compressed,
..rsass::output::Format::default()
},
)
.expect("SCSS compilation error"),
)?;
Ok(())
@@ -0,0 +1 @@
DROP INDEX medias_index_file_path;
@@ -0,0 +1 @@
CREATE INDEX medias_index_file_path ON medias (file_path);
@@ -0,0 +1 @@
DROP TABLE email_signups;
@@ -0,0 +1,9 @@
CREATE TABLE email_signups (
id SERIAL PRIMARY KEY,
email VARCHAR NOT NULL,
token VARCHAR NOT NULL,
expiration_date TIMESTAMP NOT NULL
);
CREATE INDEX email_signups_token ON email_signups (token);
CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email);
@@ -0,0 +1 @@
DROP INDEX medias_index_file_path;
@@ -0,0 +1 @@
CREATE INDEX medias_index_file_path ON medias (file_path);
@@ -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.0"
version = "0.7.1"
authors = ["Plume contributors"]
edition = "2018"
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "plume-cli"
version = "0.6.0"
version = "0.7.1"
authors = ["Plume contributors"]
edition = "2018"
@@ -10,12 +10,12 @@ path = "src/main.rs"
[dependencies]
clap = "2.33"
dotenv = "0.14"
rpassword = "4.0"
dotenv = "0.15"
rpassword = "5.0.0"
[dependencies.diesel]
features = ["r2d2", "chrono"]
version = "*"
version = "1.4.5"
[dependencies.plume-models]
path = "../plume-models"
+2
View File
@@ -68,4 +68,6 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
},
)
.expect("Couldn't save instance");
Instance::cache_local(conn);
Instance::create_local_instance_user(conn).expect("Couldn't save local instance user");
}
+1 -3
View File
@@ -1,5 +1,3 @@
use dotenv;
use clap::App;
use diesel::Connection;
use plume_models::{instance::Instance, Connection as Conn, CONFIG};
@@ -27,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)) => {
+1 -1
View File
@@ -106,7 +106,7 @@ fn refill<'a>(args: &ArgMatches<'a>, conn: &Connection, searcher: Option<Searche
searcher.commit();
}
fn unlock<'a>(args: &ArgMatches<'a>) {
fn unlock(args: &ArgMatches) {
let path = match args.value_of("path") {
None => Path::new(&CONFIG.search_index),
Some(x) => Path::new(x),
-1
View File
@@ -1,7 +1,6 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use plume_models::{instance::Instance, users::*, Connection};
use rpassword;
use std::io::{self, Write};
pub fn command<'a, 'b>() -> App<'a, 'b> {
+16 -10
View File
@@ -1,6 +1,6 @@
[package]
name = "plume-common"
version = "0.6.0"
version = "0.7.1"
authors = ["Plume contributors"]
edition = "2018"
@@ -9,20 +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.5"
reqwest = "0.9"
rocket = "0.4.6"
reqwest = { version = "0.9", features = ["socks"] }
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
shrinkwraprs = "0.2.1"
syntect = "3.3"
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"]
@@ -32,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]
+187 -33
View File
@@ -1,6 +1,8 @@
use reqwest::header::{HeaderValue, ACCEPT};
use reqwest;
use std::fmt::Debug;
use super::{request, sign::Signer};
/// Represents an ActivityPub inbox.
///
/// It routes an incoming Activity through the registered handlers.
@@ -10,7 +12,50 @@ use std::fmt::Debug;
/// ```rust
/// # extern crate activitypub;
/// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note};
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
/// # use once_cell::sync::Lazy;
/// # use plume_common::activity_pub::inbox::*;
/// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer};
/// #
/// # static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
/// #
/// # struct MySigner {
/// # public_key: String,
/// # private_key: String,
/// # }
/// #
/// # impl MySigner {
/// # fn new() -> Self {
/// # let (pub_key, priv_key) = gen_keypair();
/// # Self {
/// # public_key: String::from_utf8(pub_key).unwrap(),
/// # private_key: String::from_utf8(priv_key).unwrap(),
/// # }
/// # }
/// # }
/// #
/// # impl Signer for MySigner {
/// # fn get_key_id(&self) -> String {
/// # "mysigner".into()
/// # }
/// #
/// # fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
/// # let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
/// # .unwrap();
/// # let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
/// # signer.update(to_sign.as_bytes()).unwrap();
/// # signer.sign_to_vec().map_err(|_| SignError())
/// # }
/// #
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
/// # let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
/// # .unwrap();
/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
/// # verifier.update(data.as_bytes()).unwrap();
/// # verifier.verify(&signature).map_err(|_| SignError())
/// # }
/// # }
/// #
/// # struct User;
/// # impl FromId<()> for User {
/// # type Error = ();
@@ -23,6 +68,10 @@ use std::fmt::Debug;
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
/// # Ok(User)
/// # }
/// #
/// # fn get_sender() -> &'static dyn Signer {
/// # &*MY_SIGNER
/// # }
/// # }
/// # impl AsActor<&()> for User {
/// # fn get_inbox_url(&self) -> String {
@@ -42,6 +91,10 @@ use std::fmt::Debug;
/// # fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
/// # Ok(Message)
/// # }
/// #
/// # fn get_sender() -> &'static dyn Signer {
/// # &*MY_SIGNER
/// # }
/// # }
/// # impl AsObject<User, Create, &()> for Message {
/// # type Error = ();
@@ -71,8 +124,8 @@ use std::fmt::Debug;
/// # let conn = ();
/// #
/// let result: Result<(), ()> = Inbox::handle(&conn, activity_json)
/// .with::<User, Announce, Message>()
/// .with::<User, Create, Message>()
/// .with::<User, Announce, Message>(None)
/// .with::<User, Create, Message>(None)
/// .done();
/// ```
pub enum Inbox<'a, C, E, R>
@@ -144,7 +197,7 @@ where
}
/// Registers an handler on this Inbox.
pub fn with<A, V, M>(self) -> Inbox<'a, C, E, R>
pub fn with<A, V, M>(self, proxy: Option<&reqwest::Proxy>) -> Inbox<'a, C, E, R>
where
A: AsActor<&'a C> + FromId<C, Error = E>,
V: activitypub::Activity,
@@ -174,6 +227,7 @@ where
ctx,
&actor_id,
serde_json::from_value(act["actor"].clone()).ok(),
proxy,
) {
Ok(a) => a,
// If the actor was not found, go to the next handler
@@ -194,6 +248,7 @@ where
ctx,
&obj_id,
serde_json::from_value(act["object"].clone()).ok(),
proxy,
) {
Ok(o) => o,
Err((json, e)) => {
@@ -205,7 +260,7 @@ where
};
// Handle the activity
match obj.activity(ctx, actor, &act_id) {
match obj.activity(ctx, actor, act_id) {
Ok(res) => Inbox::Handled(res.into()),
Err(e) => Inbox::Failed(e),
}
@@ -292,34 +347,24 @@ pub trait FromId<C>: Sized {
ctx: &C,
id: &str,
object: Option<Self::Object>,
proxy: Option<&reqwest::Proxy>,
) -> Result<Self, (Option<serde_json::Value>, Self::Error)> {
match Self::from_db(ctx, id) {
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)?).map_err(|e| (None, e)),
None => Self::from_activity(ctx, Self::deref(id, proxy.cloned())?)
.map_err(|e| (None, e)),
},
}
}
/// Dereferences an ID
fn deref(id: &str) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
reqwest::ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()
.map_err(|_| (None, InboxError::DerefError.into()))?
.get(id)
.header(
ACCEPT,
HeaderValue::from_str(
&super::ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)
.map_err(|_| (None, InboxError::DerefError.into()))?,
)
.send()
fn deref(
id: &str,
proxy: Option<reqwest::Proxy>,
) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
request::get(id, Self::get_sender(), proxy)
.map_err(|_| (None, InboxError::DerefError))
.and_then(|mut r| {
let json: serde_json::Value = r
@@ -336,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.
@@ -374,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 = ();
@@ -386,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 {
@@ -409,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 {
@@ -448,7 +546,51 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::activity_pub::sign::{
gen_keypair, Error as SignError, Result as SignResult, Signer,
};
use activitypub::{activity::*, actor::Person, object::Note};
use once_cell::sync::Lazy;
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
struct MySigner {
public_key: String,
private_key: String,
}
impl MySigner {
fn new() -> Self {
let (pub_key, priv_key) = gen_keypair();
Self {
public_key: String::from_utf8(pub_key).unwrap(),
private_key: String::from_utf8(priv_key).unwrap(),
}
}
}
impl Signer for MySigner {
fn get_key_id(&self) -> String {
"mysigner".into()
}
fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
.unwrap();
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
signer.update(to_sign.as_bytes()).unwrap();
signer.sign_to_vec().map_err(|_| SignError())
}
fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
.unwrap();
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
verifier.update(data.as_bytes()).unwrap();
verifier.verify(&signature).map_err(|_| SignError())
}
}
struct MyActor;
impl FromId<()> for MyActor {
@@ -462,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 {
@@ -486,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 = ();
@@ -550,7 +700,7 @@ mod tests {
fn test_inbox_basic() {
let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Create, MyObject>()
.with::<MyActor, Create, MyObject>(None)
.done();
assert!(res.is_ok());
}
@@ -559,10 +709,10 @@ mod tests {
fn test_inbox_multi_handlers() {
let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Announce, MyObject>()
.with::<MyActor, Delete, MyObject>()
.with::<MyActor, Create, MyObject>()
.with::<MyActor, Like, MyObject>()
.with::<MyActor, Announce, MyObject>(None)
.with::<MyActor, Delete, MyObject>(None)
.with::<MyActor, Create, MyObject>(None)
.with::<MyActor, Like, MyObject>(None)
.done();
assert!(res.is_ok());
}
@@ -572,8 +722,8 @@ mod tests {
let act = serde_json::to_value(build_create()).unwrap();
// Create is not handled by this inbox
let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Announce, MyObject>()
.with::<MyActor, Like, MyObject>()
.with::<MyActor, Announce, MyObject>(None)
.with::<MyActor, Like, MyObject>(None)
.done();
assert!(res.is_err());
}
@@ -590,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 {
@@ -621,13 +775,13 @@ mod tests {
let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
.with::<FailingActor, Create, MyObject>()
.with::<FailingActor, Create, MyObject>(None)
.done();
assert!(res.is_err());
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
.with::<FailingActor, Create, MyObject>()
.with::<MyActor, Create, MyObject>()
.with::<FailingActor, Create, MyObject>(None)
.with::<MyActor, Create, MyObject>(None)
.done();
assert!(res.is_ok());
}
+46 -23
View File
@@ -1,14 +1,14 @@
use activitypub::{Activity, Link, Object};
use array_tool::vec::Uniq;
use reqwest::r#async::ClientBuilder;
use reqwest::{header::HeaderValue, r#async::ClientBuilder, Url};
use rocket::{
http::Status,
request::{FromRequest, Request},
response::{Responder, Response},
Outcome,
};
use serde_json;
use tokio::prelude::*;
use tracing::{debug, warn};
use self::sign::Signable;
@@ -108,7 +108,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
.unwrap_or(Outcome::Forward(()))
}
}
pub fn broadcast<S, A, T, C>(sender: &S, act: A, to: Vec<T>)
pub fn broadcast<S, A, T, C>(sender: &S, act: A, to: Vec<T>, proxy: Option<reqwest::Proxy>)
where
S: sign::Signer,
A: Activity,
@@ -132,31 +132,54 @@ where
let mut rt = tokio::runtime::current_thread::Runtime::new()
.expect("Error while initializing tokio runtime for federation");
let client = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5))
.build()
.expect("Can't build client");
for inbox in boxes {
let body = signed.to_string();
let mut headers = request::headers();
let url = Url::parse(&inbox);
if url.is_err() {
warn!("Inbox is invalid URL: {:?}", &inbox);
continue;
}
let url = url.unwrap();
if !url.has_host() {
warn!("Inbox doesn't have host: {:?}", &inbox);
continue;
};
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;
}
headers.insert("Host", host_header_value.unwrap());
headers.insert("Digest", request::Digest::digest(&body));
rt.spawn(
client
.post(&inbox)
.headers(headers.clone())
.header(
"Signature",
request::signature(sender, &headers)
.expect("activity_pub::broadcast: request signature error"),
)
.body(body)
.send()
.and_then(|r| r.into_body().concat2())
.map(move |response| {
println!("Successfully sent activity to inbox ({})", inbox);
println!("Response: \"{:?}\"\n", response)
})
.map_err(|e| println!("Error while sending to inbox ({:?})", e)),
if let Some(proxy) = proxy.clone() {
ClientBuilder::new().proxy(proxy)
} else {
ClientBuilder::new()
}
.connect_timeout(std::time::Duration::from_secs(5))
.build()
.expect("Can't build client")
.post(&inbox)
.headers(headers.clone())
.header(
"Signature",
request::signature(sender, &headers, ("post", url.path(), url.query()))
.expect("activity_pub::broadcast: request signature error"),
)
.body(body)
.send()
.and_then(move |r| {
if r.status().is_success() {
debug!("Successfully sent activity to inbox ({})", &inbox);
} else {
warn!("Error while sending to inbox ({:?})", &r)
}
r.into_body().concat2()
})
.map(move |response| debug!("Response: \"{:?}\"\n", response))
.map_err(|e| warn!("Error while sending to inbox ({:?})", e)),
);
}
rt.run().unwrap();
+148 -21
View File
@@ -1,15 +1,41 @@
use base64;
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;
use crate::activity_pub::sign::Signer;
use crate::activity_pub::{ap_accept_header, AP_CONTENT_TYPE};
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 {
@@ -62,16 +88,16 @@ impl Digest {
base64::decode(&self.0[pos..]).expect("Digest::value: invalid encoding error")
}
pub fn from_header(dig: &str) -> Result<Self, ()> {
pub fn from_header(dig: &str) -> Result<Self, Error> {
if let Some(pos) = dig.find('=') {
let pos = pos + 1;
if base64::decode(&dig[pos..]).is_ok() {
Ok(Digest(dig.to_owned()))
} else {
Err(())
Err(Error())
}
} else {
Err(())
Err(Error())
}
}
@@ -110,27 +136,47 @@ pub fn headers() -> HeaderMap {
headers
}
pub fn signature<S: Signer>(signer: &S, headers: &HeaderMap) -> Result<HeaderValue, ()> {
let signed_string = headers
type Method<'a> = &'a str;
type Path<'a> = &'a str;
type Query<'a> = &'a str;
type RequestTarget<'a> = (Method<'a>, Path<'a>, Option<Query<'a>>);
pub fn signature(
signer: &dyn Signer,
headers: &HeaderMap,
request_target: RequestTarget,
) -> Result<HeaderValue, Error> {
let (method, path, query) = request_target;
let origin_form = if let Some(query) = query {
format!("{}?{}", path, query)
} else {
path.to_string()
};
let mut headers_vec = Vec::with_capacity(headers.len());
for (h, v) in headers.iter() {
let v = v.to_str();
if v.is_err() {
warn!("invalid header error: {:?}", v.unwrap_err());
return Err(Error());
}
headers_vec.push((h.as_str().to_lowercase(), v.expect("Unreachable")));
}
let request_target = format!("{} {}", method.to_lowercase(), origin_form);
headers_vec.push(("(request-target)".to_string(), &request_target));
let signed_string = headers_vec
.iter()
.map(|(h, v)| {
format!(
"{}: {}",
h.as_str().to_lowercase(),
v.to_str()
.expect("request::signature: invalid header error")
)
})
.map(|(h, v)| format!("{}: {}", h, v))
.collect::<Vec<String>>()
.join("\n");
let signed_headers = headers
let signed_headers = headers_vec
.iter()
.map(|(h, _)| h.as_str())
.map(|(h, _)| h.as_ref())
.collect::<Vec<&str>>()
.join(" ")
.to_lowercase();
.join(" ");
let data = signer.sign(&signed_string).map_err(|_| ())?;
let data = signer.sign(&signed_string).map_err(|_| Error())?;
let sign = base64::encode(&data);
HeaderValue::from_str(&format!(
@@ -138,5 +184,86 @@ pub fn signature<S: Signer>(signer: &S, headers: &HeaderMap) -> Result<HeaderVal
key_id = signer.get_key_id(),
signed_headers = signed_headers,
signature = sign
)).map_err(|_| ())
)).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, Result, Signer};
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
use reqwest::header::HeaderMap;
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) -> 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(|_| Error())
}
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
.unwrap();
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
verifier.update(data.as_bytes()).unwrap();
verifier.verify(&signature).map_err(|_| Error())
}
}
#[test]
fn test_signature_request_target() {
let signer = MySigner::new();
let headers = HeaderMap::new();
let result = signature(&signer, &headers, ("post", "/inbox", None)).unwrap();
let fields: Vec<&str> = result.to_str().unwrap().split(",").collect();
assert_eq!(r#"headers="(request-target)""#, fields[2]);
let sign = &fields[3][11..(fields[3].len() - 1)];
assert!(signer.verify("post /inbox", sign.as_bytes()).is_ok());
}
}
+16 -11
View File
@@ -1,10 +1,7 @@
use super::request;
use base64;
use chrono::{naive::NaiveDateTime, DateTime, Duration, Utc};
use hex;
use openssl::{pkey::PKey, rsa::Rsa, sha::sha256};
use rocket::http::HeaderMap;
use serde_json;
/// Returns (public key, private key)
pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
@@ -20,19 +17,27 @@ pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
)
}
pub trait Signer {
type Error;
#[derive(Debug)]
pub struct Error();
pub type Result<T> = std::result::Result<T, Error>;
impl From<openssl::error::ErrorStack> for Error {
fn from(_: openssl::error::ErrorStack) -> Self {
Self()
}
}
pub trait Signer {
fn get_key_id(&self) -> String;
/// Sign some data with the signer keypair
fn sign(&self, to_sign: &str) -> Result<Vec<u8>, Self::Error>;
fn sign(&self, to_sign: &str) -> Result<Vec<u8>>;
/// Verify if the signature is valid
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error>;
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool>;
}
pub trait Signable {
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self, ()>
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self>
where
T: Signer;
fn verify<T>(self, creator: &T) -> bool
@@ -46,7 +51,7 @@ pub trait Signable {
}
impl Signable for serde_json::Value {
fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value, ()> {
fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value> {
let creation_date = Utc::now().to_rfc3339();
let mut options = json!({
"type": "RsaSignature2017",
@@ -64,7 +69,7 @@ impl Signable for serde_json::Value {
let document_hash = Self::hash(&self.to_string());
let to_be_signed = options_hash + &document_hash;
let signature = base64::encode(&creator.sign(&to_be_signed).map_err(|_| ())?);
let signature = base64::encode(&creator.sign(&to_be_signed).map_err(|_| Error())?);
options["signatureValue"] = serde_json::Value::String(signature);
self["signature"] = options;
@@ -182,7 +187,7 @@ pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
}
let digest = all_headers.get_one("digest").unwrap_or("");
let digest = request::Digest::from_header(digest);
if !digest.map(|d| d.verify_header(&data)).unwrap_or(false) {
if !digest.map(|d| d.verify_header(data)).unwrap_or(false) {
// signature was valid, but body content does not match its digest
return SignatureValidity::Invalid;
}
Regular → Executable
-3
View File
@@ -2,9 +2,6 @@
#[macro_use]
extern crate activitystreams_derive;
use activitystreams_traits;
use serde;
#[macro_use]
extern crate shrinkwraprs;
#[macro_use]
+85 -24
View File
@@ -1,13 +1,10 @@
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::ClassedHTMLGenerator;
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
use syntect::parsing::SyntaxSet;
/// Generates an hexadecimal representation of 32 bytes of random data
@@ -21,23 +18,63 @@ pub fn random_hex() -> String {
/// Remove non alphanumeric characters and CamelCase a string
pub fn make_actor_id(name: &str) -> String {
name.to_camel_case()
name.to_upper_camel_case()
.chars()
.filter(|c| c.is_alphanumeric())
.collect()
}
/**
* Redirects to the login page with a given message.
*
* Note that the message should be translated before passed to this function.
*/
pub fn requires_login<T: Into<Uri<'static>>>(message: &str, url: T) -> Flash<Redirect> {
Flash::new(
Redirect::to(format!("/login?m={}", Uri::percent_encode(message))),
"callback",
url.into().to_string(),
)
* Percent-encode characters which are not allowed in IRI path segments.
*
* Intended to be used for generating Post ap_url.
*/
pub fn iri_percent_encode_seg(segment: &str) -> String {
segment.chars().map(iri_percent_encode_seg_char).collect()
}
pub fn iri_percent_encode_seg_char(c: char) -> String {
if c.is_alphanumeric() {
c.to_string()
} else {
match c {
'-'
| '.'
| '_'
| '~'
| '\u{A0}'..='\u{D7FF}'
| '\u{20000}'..='\u{2FFFD}'
| '\u{30000}'..='\u{3FFFD}'
| '\u{40000}'..='\u{4FFFD}'
| '\u{50000}'..='\u{5FFFD}'
| '\u{60000}'..='\u{6FFFD}'
| '\u{70000}'..='\u{7FFFD}'
| '\u{80000}'..='\u{8FFFD}'
| '\u{90000}'..='\u{9FFFD}'
| '\u{A0000}'..='\u{AFFFD}'
| '\u{B0000}'..='\u{BFFFD}'
| '\u{C0000}'..='\u{CFFFD}'
| '\u{D0000}'..='\u{DFFFD}'
| '\u{E0000}'..='\u{EFFFD}'
| '!'
| '$'
| '&'
| '\''
| '('
| ')'
| '*'
| '+'
| ','
| ';'
| '='
| ':'
| '@' => c.to_string(),
_ => {
let s = c.to_string();
Uri::percent_encode(&s).to_string()
}
}
}
}
#[derive(Debug)]
@@ -60,6 +97,7 @@ fn to_inline(tag: Tag<'_>) -> Tag<'_> {
struct HighlighterContext {
content: Vec<String>,
}
#[allow(clippy::unnecessary_wraps)]
fn highlight_code<'a>(
context: &mut Option<HighlighterContext>,
evt: Event<'a>,
@@ -87,14 +125,18 @@ 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(&syntax, &syntax_set);
let mut html = ClassedHTMLGenerator::new_with_class_style(
syntax,
&syntax_set,
ClassStyle::Spaced,
);
for line in ctx.content {
html.parse_html_for_line(&line);
html.parse_html_for_line_which_includes_newline(&line);
}
let q = html.finalize();
result.push(Event::Html(q.into()));
@@ -115,6 +157,7 @@ fn highlight_code<'a>(
_ => Some(vec![evt]),
}
}
#[allow(clippy::unnecessary_wraps)]
fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Event<'a>>> {
let (s, res) = match evt {
Event::Text(txt) => match state.take() {
@@ -133,6 +176,7 @@ fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Ev
Some(res)
}
#[allow(clippy::unnecessary_wraps)]
fn inline_tags<'a>(
(state, inline): &mut (Vec<Tag<'a>>, bool),
evt: Event<'a>,
@@ -274,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));
(
@@ -407,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::*;
@@ -469,6 +516,20 @@ mod tests {
}
}
#[test]
fn test_iri_percent_encode_seg() {
assert_eq!(
&iri_percent_encode_seg("including whitespace"),
"including%20whitespace"
);
assert_eq!(&iri_percent_encode_seg("%20"), "%2520");
assert_eq!(&iri_percent_encode_seg("é"), "é");
assert_eq!(
&iri_percent_encode_seg("空白入り 日本語"),
"空白入り%20日本語"
);
}
#[test]
fn test_inline() {
assert_eq!(
+40 -3
View File
@@ -1,15 +1,52 @@
[package]
name = "plume-front"
version = "0.6.0"
version = "0.7.1"
authors = ["Plume contributors"]
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
stdweb = "=0.4.18"
stdweb-internal-runtime = "=0.1.4"
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" }
lazy_static = "1.3"
serde = "1.0"
serde_json = "1.0"
wasm-bindgen = "0.2.70"
js-sys = "0.3.47"
serde_derive = "1.0.123"
console_error_panic_hook = "0.1.6"
[dependencies.web-sys]
version = "0.3.47"
features = [
'console',
'ClipboardEvent',
'CssStyleDeclaration',
'DataTransfer',
'Document',
'DomStringMap',
'DomTokenList',
'Element',
'EventTarget',
'FocusEvent',
'History',
'HtmlAnchorElement',
'HtmlDocument',
'HtmlFormElement',
'HtmlInputElement',
'HtmlSelectElement',
'HtmlTextAreaElement',
'KeyboardEvent',
'Storage',
'Location',
'MouseEvent',
'Navigator',
'Node',
'NodeList',
'Text',
'TouchEvent',
'Window'
]
+439 -273
View File
@@ -1,10 +1,12 @@
use crate::CATALOG;
use serde::{Deserialize, Serialize};
use serde_json;
use std::sync::Mutex;
use stdweb::{
unstable::{TryFrom, TryInto},
web::{event::*, html_element::*, *},
use crate::{document, CATALOG};
use js_sys::{encode_uri_component, Date, RegExp};
use serde_derive::{Deserialize, Serialize};
use std::{convert::TryInto, sync::Mutex};
use wasm_bindgen::{prelude::*, JsCast, JsValue};
use web_sys::{
console, window, ClipboardEvent, Element, Event, FocusEvent, HtmlAnchorElement, HtmlDocument,
HtmlElement, HtmlFormElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement,
KeyboardEvent, MouseEvent, Node,
};
macro_rules! mv {
@@ -18,30 +20,29 @@ macro_rules! mv {
fn get_elt_value(id: &'static str) -> String {
let elt = document().get_element_by_id(id).unwrap();
let inp: Result<InputElement, _> = elt.clone().try_into();
let textarea: Result<TextAreaElement, _> = elt.clone().try_into();
let select: Result<SelectElement, _> = elt.try_into();
inp.map(|i| i.raw_value()).unwrap_or_else(|_| {
let inp: Option<&HtmlInputElement> = elt.dyn_ref();
let textarea: Option<&HtmlTextAreaElement> = elt.dyn_ref();
let select: Option<&HtmlSelectElement> = elt.dyn_ref();
inp.map(|i| i.value()).unwrap_or_else(|| {
textarea
.map(|t| t.value())
.unwrap_or_else(|_| select.unwrap().raw_value())
.unwrap_or_else(|| select.unwrap().value())
})
}
fn set_value<S: AsRef<str>>(id: &'static str, val: S) {
let elt = document().get_element_by_id(id).unwrap();
let inp: Result<InputElement, _> = elt.clone().try_into();
let textarea: Result<TextAreaElement, _> = elt.clone().try_into();
let select: Result<SelectElement, _> = elt.try_into();
inp.map(|i| i.set_raw_value(val.as_ref()))
.unwrap_or_else(|_| {
textarea
.map(|t| t.set_value(val.as_ref()))
.unwrap_or_else(|_| select.unwrap().set_raw_value(val.as_ref()))
})
let inp: Option<&HtmlInputElement> = elt.dyn_ref();
let textarea: Option<&HtmlTextAreaElement> = elt.dyn_ref();
let select: Option<&HtmlSelectElement> = elt.dyn_ref();
inp.map(|i| i.set_value(val.as_ref())).unwrap_or_else(|| {
textarea
.map(|t| t.set_value(val.as_ref()))
.unwrap_or_else(|| select.unwrap().set_value(val.as_ref()))
})
}
fn no_return(evt: KeyDownEvent) {
fn no_return(evt: KeyboardEvent) {
if evt.key() == "Enter" {
evt.prevent_default();
}
@@ -51,30 +52,9 @@ fn no_return(evt: KeyDownEvent) {
pub enum EditorError {
NoneError,
DOMError,
TypeError,
}
impl From<std::option::NoneError> for EditorError {
fn from(_: std::option::NoneError) -> Self {
EditorError::NoneError
}
}
impl From<stdweb::web::error::InvalidCharacterError> for EditorError {
fn from(_: stdweb::web::error::InvalidCharacterError) -> Self {
EditorError::DOMError
}
}
impl From<stdweb::private::TODO> for EditorError {
fn from(_: stdweb::private::TODO) -> Self {
EditorError::DOMError
}
}
impl From<stdweb::private::ConversionError> for EditorError {
fn from(_: stdweb::private::ConversionError) -> Self {
EditorError::TypeError
}
}
const AUTOSAVE_DEBOUNCE_TIME: u32 = 5000;
const AUTOSAVE_DEBOUNCE_TIME: i32 = 5000;
#[derive(Serialize, Deserialize)]
struct AutosaveInformation {
contents: String,
@@ -85,10 +65,16 @@ struct AutosaveInformation {
tags: String,
title: String,
}
js_serializable!(AutosaveInformation);
fn is_basic_editor() -> bool {
if let Some(basic_editor) = window().local_storage().get("basic-editor") {
basic_editor == "true"
if let Some(basic_editor) = window()
.unwrap()
.local_storage()
.unwrap()
.unwrap()
.get("basic-editor")
.unwrap()
{
&basic_editor == "true"
} else {
false
}
@@ -97,65 +83,58 @@ fn get_title() -> String {
if is_basic_editor() {
get_elt_value("title")
} else {
let title_field = HtmlElement::try_from(
document()
.query_selector("#plume-editor > h1")
.ok()
.unwrap()
.unwrap(),
)
.ok()
.unwrap();
title_field.inner_text()
document()
.query_selector("#plume-editor > h1")
.unwrap()
.unwrap()
.dyn_ref::<HtmlElement>()
.unwrap()
.inner_text()
}
}
fn get_autosave_id() -> String {
format!(
"editor_contents={}",
window().location().unwrap().pathname().unwrap()
window().unwrap().location().pathname().unwrap()
)
}
fn get_editor_contents() -> String {
if is_basic_editor() {
get_elt_value("editor-content")
} else {
let editor =
HtmlElement::try_from(document().query_selector("article").ok().unwrap().unwrap())
.ok()
.unwrap();
editor.child_nodes().iter().fold(String::new(), |md, ch| {
let editor = document().query_selector("article").unwrap().unwrap();
let child_nodes = editor.child_nodes();
let mut md = String::new();
for i in 0..child_nodes.length() {
let ch = child_nodes.get(i).unwrap();
let to_append = match ch.node_type() {
NodeType::Element => {
if js! { return @{&ch}.tagName; } == "DIV" {
(js! { return @{&ch}.innerHTML; })
.try_into()
.unwrap_or_default()
Node::ELEMENT_NODE => {
let elt = ch.dyn_ref::<Element>().unwrap();
if elt.tag_name() == "DIV" {
elt.inner_html()
} else {
(js! { return @{&ch}.outerHTML; })
.try_into()
.unwrap_or_default()
elt.outer_html()
}
}
NodeType::Text => ch.node_value().unwrap_or_default(),
Node::TEXT_NODE => ch.node_value().unwrap_or_default(),
_ => unreachable!(),
};
format!("{}\n\n{}", md, to_append)
})
md = format!("{}\n\n{}", md, to_append);
}
md
}
}
fn get_subtitle() -> String {
if is_basic_editor() {
get_elt_value("subtitle")
} else {
let subtitle_element = HtmlElement::try_from(
document()
.query_selector("#plume-editor > h2")
.unwrap()
.unwrap(),
)
.ok()
.unwrap();
subtitle_element.inner_text()
document()
.query_selector("#plume-editor > h2")
.unwrap()
.unwrap()
.dyn_ref::<HtmlElement>()
.unwrap()
.inner_text()
}
}
fn autosave() {
@@ -170,27 +149,31 @@ fn autosave() {
};
let id = get_autosave_id();
match window()
.unwrap()
.local_storage()
.insert(&id, &serde_json::to_string(&info).unwrap())
.unwrap()
.unwrap()
.set(&id, &serde_json::to_string(&info).unwrap())
{
Ok(_) => {}
_ => console!(log, "Autosave failed D:"),
_ => console::log_1(&"Autosave failed D:".into()),
}
}
//This is only necessary until we go to stdweb 4.20 at least
fn confirm(message: &str) -> bool {
let result: bool = js! {return confirm(@{message});} == true;
result
}
fn load_autosave() {
if let Some(autosave_str) = window().local_storage().get(&get_autosave_id()) {
if let Ok(Some(autosave_str)) = window()
.unwrap()
.local_storage()
.unwrap()
.unwrap()
.get(&get_autosave_id())
{
let autosave_info: AutosaveInformation = serde_json::from_str(&autosave_str).ok().unwrap();
let message = i18n!(
CATALOG,
"Do you want to load the local autosave last edited at {}?";
Date::from_time(autosave_info.last_saved).to_date_string()
Date::new(&JsValue::from_f64(autosave_info.last_saved)).to_date_string().as_string().unwrap()
);
if confirm(&message) {
if let Ok(true) = window().unwrap().confirm_with_message(&message) {
set_value("editor-content", &autosave_info.contents);
set_value("title", &autosave_info.title);
set_value("subtitle", &autosave_info.subtitle);
@@ -203,18 +186,33 @@ fn load_autosave() {
}
}
fn clear_autosave() {
window().local_storage().remove(&get_autosave_id());
console!(log, &format!("Saved to {}", &get_autosave_id()));
window()
.unwrap()
.local_storage()
.unwrap()
.unwrap()
.remove_item(&get_autosave_id())
.unwrap();
console::log_1(&format!("Saved to {}", &get_autosave_id()).into());
}
type TimeoutHandle = i32;
lazy_static! {
static ref AUTOSAVE_TIMEOUT: Mutex<Option<TimeoutHandle>> = Mutex::new(None);
}
fn autosave_debounce() {
let window = window().unwrap();
let timeout = &mut AUTOSAVE_TIMEOUT.lock().unwrap();
if let Some(timeout) = timeout.take() {
timeout.clear();
window.clear_timeout_with_handle(timeout);
}
**timeout = Some(window().set_clearable_timeout(autosave, AUTOSAVE_DEBOUNCE_TIME));
let callback = Closure::once(autosave);
**timeout = window
.set_timeout_with_callback_and_timeout_and_arguments_0(
callback.as_ref().unchecked_ref(),
AUTOSAVE_DEBOUNCE_TIME,
)
.ok();
callback.forget();
}
fn init_widget(
parent: &Element,
@@ -223,19 +221,33 @@ fn init_widget(
content: String,
disable_return: bool,
) -> Result<HtmlElement, EditorError> {
let widget = placeholder(make_editable(tag).try_into()?, &placeholder_text);
let widget = placeholder(
make_editable(tag).dyn_into::<HtmlElement>().unwrap(),
&placeholder_text,
);
if !content.is_empty() {
widget.dataset().insert("edited", "true")?;
widget
.dataset()
.set("edited", "true")
.map_err(|_| EditorError::DOMError)?;
}
widget.append_child(&document().create_text_node(&content));
widget
.append_child(&document().create_text_node(&content))
.map_err(|_| EditorError::DOMError)?;
if disable_return {
widget.add_event_listener(no_return);
let callback = Closure::wrap(Box::new(no_return) as Box<dyn FnMut(KeyboardEvent)>);
widget
.add_event_listener_with_callback("keydown", callback.as_ref().unchecked_ref())
.map_err(|_| EditorError::DOMError)?;
callback.forget();
}
parent.append_child(&widget);
parent
.append_child(&widget)
.map_err(|_| EditorError::DOMError)?;
// We need to do that to make sure the placeholder is correctly rendered
widget.focus();
widget.blur();
widget.focus().map_err(|_| EditorError::DOMError)?;
widget.blur().map_err(|_| EditorError::DOMError)?;
filter_paste(&widget);
@@ -244,48 +256,88 @@ fn init_widget(
fn filter_paste(elt: &HtmlElement) {
// Only insert text when pasting something
js! {
@{&elt}.addEventListener("paste", function (evt) {
evt.preventDefault();
document.execCommand("insertText", false, evt.clipboardData.getData("text"));
});
};
let insert_text = Closure::wrap(Box::new(|evt: ClipboardEvent| {
evt.prevent_default();
if let Some(data) = evt.clipboard_data() {
if let Ok(data) = data.get_data("text") {
document()
.dyn_ref::<HtmlDocument>()
.unwrap()
.exec_command_with_show_ui_and_value("insertText", false, &data)
.unwrap();
}
}
}) as Box<dyn FnMut(ClipboardEvent)>);
elt.add_event_listener_with_callback("paste", insert_text.as_ref().unchecked_ref())
.unwrap();
insert_text.forget();
}
pub fn init() -> Result<(), EditorError> {
if let Some(ed) = document().get_element_by_id("plume-fallback-editor") {
load_autosave();
ed.add_event_listener(|_: SubmitEvent| clear_autosave());
let callback = Closure::wrap(Box::new(|_| clear_autosave()) as Box<dyn FnMut(Event)>);
ed.add_event_listener_with_callback("submit", callback.as_ref().unchecked_ref())
.map_err(|_| EditorError::DOMError)?;
callback.forget();
}
// Check if the user wants to use the basic editor
if window()
.unwrap()
.local_storage()
.unwrap()
.unwrap()
.get("basic-editor")
.map(|x| x == "true")
.map(|x| x.is_some() && x.unwrap() == "true")
.unwrap_or(true)
{
if let Some(editor) = document().get_element_by_id("plume-fallback-editor") {
if let Ok(Some(title_label)) = document().query_selector("label[for=title]") {
let editor_button = document().create_element("a")?;
js! { @{&editor_button}.href = "#"; }
editor_button.add_event_listener(|_: ClickEvent| {
if window()
let editor_button = document()
.create_element("a")
.map_err(|_| EditorError::DOMError)?;
editor_button
.dyn_ref::<HtmlAnchorElement>()
.unwrap()
.set_href("#");
let disable_basic_editor = Closure::wrap(Box::new(|_| {
let window = window().unwrap();
if window
.local_storage()
.insert("basic-editor", "false")
.unwrap()
.unwrap()
.set("basic-editor", "false")
.is_err()
{
console!(log, "Failed to write into local storage");
console::log_1(&"Failed to write into local storage".into());
}
window().history().go(0).ok(); // refresh
});
editor_button.append_child(
&document().create_text_node(&i18n!(CATALOG, "Open the rich text editor")),
window.history().unwrap().go_with_delta(0).ok(); // refresh
})
as Box<dyn FnMut(MouseEvent)>);
editor_button
.add_event_listener_with_callback(
"click",
disable_basic_editor.as_ref().unchecked_ref(),
)
.map_err(|_| EditorError::DOMError)?;
disable_basic_editor.forget();
editor_button
.append_child(
&document().create_text_node(&i18n!(CATALOG, "Open the rich text editor")),
)
.map_err(|_| EditorError::DOMError)?;
editor
.insert_before(&editor_button, Some(&title_label))
.ok();
let callback = Closure::wrap(
Box::new(|_| autosave_debounce()) as Box<dyn FnMut(KeyboardEvent)>
);
editor.insert_before(&editor_button, &title_label).ok();
document()
.get_element_by_id("editor-content")
.unwrap()
.add_event_listener(|_: KeyDownEvent| autosave_debounce());
.add_event_listener_with_callback("keydown", callback.as_ref().unchecked_ref())
.map_err(|_| EditorError::DOMError)?;
callback.forget();
}
}
@@ -298,14 +350,32 @@ pub fn init() -> Result<(), EditorError> {
fn init_editor() -> Result<(), EditorError> {
if let Some(ed) = document().get_element_by_id("plume-editor") {
// Show the editor
js! { @{&ed}.style.display = "block"; };
ed.dyn_ref::<HtmlElement>()
.unwrap()
.style()
.set_property("display", "block")
.map_err(|_| EditorError::DOMError)?;
// And hide the HTML-only fallback
let old_ed = document().get_element_by_id("plume-fallback-editor")?;
let old_title = document().get_element_by_id("plume-editor-title")?;
js! {
@{&old_ed}.style.display = "none";
@{&old_title}.style.display = "none";
};
let old_ed = document().get_element_by_id("plume-fallback-editor");
if old_ed.is_none() {
return Ok(());
}
let old_ed = old_ed.unwrap();
let old_title = document()
.get_element_by_id("plume-editor-title")
.ok_or(EditorError::NoneError)?;
old_ed
.dyn_ref::<HtmlElement>()
.unwrap()
.style()
.set_property("display", "none")
.map_err(|_| EditorError::DOMError)?;
old_title
.dyn_ref::<HtmlElement>()
.unwrap()
.style()
.set_property("display", "none")
.map_err(|_| EditorError::DOMError)?;
// Get content from the old editor (when editing an article for instance)
let title_val = get_elt_value("title");
@@ -327,35 +397,45 @@ fn init_editor() -> Result<(), EditorError> {
content_val.clone(),
false,
)?;
js! { @{&content}.innerHTML = @{content_val}; };
content.set_inner_html(&content_val);
// character counter
content.add_event_listener(mv!(content => move |_: KeyDownEvent| {
window().set_timeout(mv!(content => move || {
let character_counter = Closure::wrap(Box::new(mv!(content => move |_| {
let update_char_count = Closure::wrap(Box::new(mv!(content => move || {
if let Some(e) = document().get_element_by_id("char-count") {
let count = chars_left("#plume-fallback-editor", &content).unwrap_or_default();
let text = i18n!(CATALOG, "Around {} characters left"; count);
HtmlElement::try_from(e).map(|e| {
js!{@{e}.innerText = @{text}};
}).ok();
e.dyn_ref::<HtmlElement>().map(|e| {
e.set_inner_text(&text);
}).unwrap();
};
}), 0);
})) as Box<dyn FnMut()>);
window().unwrap().set_timeout_with_callback_and_timeout_and_arguments(update_char_count.as_ref().unchecked_ref(), 0, &js_sys::Array::new()).unwrap();
update_char_count.forget();
autosave_debounce();
}));
})) as Box<dyn FnMut(KeyboardEvent)>);
content
.add_event_listener_with_callback("keydown", character_counter.as_ref().unchecked_ref())
.map_err(|_| EditorError::DOMError)?;
character_counter.forget();
document().get_element_by_id("publish")?.add_event_listener(
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
let popup = document().get_element_by_id("publish-popup").or_else(||
init_popup(&title, &subtitle, &content, &old_ed).ok()
).unwrap();
let bg = document().get_element_by_id("popup-bg").or_else(||
init_popup_bg().ok()
).unwrap();
let show_popup = Closure::wrap(Box::new(mv!(title, subtitle, content, old_ed => move |_| {
let popup = document().get_element_by_id("publish-popup").or_else(||
init_popup(&title, &subtitle, &content, &old_ed).ok()
).unwrap();
let bg = document().get_element_by_id("popup-bg").or_else(||
init_popup_bg().ok()
).unwrap();
popup.class_list().add("show").unwrap();
bg.class_list().add("show").unwrap();
}),
);
popup.class_list().add_1("show").unwrap();
bg.class_list().add_1("show").unwrap();
})) as Box<dyn FnMut(MouseEvent)>);
document()
.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();
show_errors();
setup_close_button();
@@ -365,32 +445,47 @@ fn init_editor() -> Result<(), EditorError> {
fn setup_close_button() {
if let Some(button) = document().get_element_by_id("close-editor") {
button.add_event_listener(|_: ClickEvent| {
let close_editor = Closure::wrap(Box::new(|_| {
window()
.unwrap()
.local_storage()
.insert("basic-editor", "true")
.unwrap()
.unwrap()
.set("basic-editor", "true")
.unwrap();
window().history().go(0).unwrap(); // Refresh the page
});
window()
.unwrap()
.history()
.unwrap()
.go_with_delta(0)
.unwrap(); // Refresh the page
}) as Box<dyn FnMut(MouseEvent)>);
button
.add_event_listener_with_callback("click", close_editor.as_ref().unchecked_ref())
.unwrap();
close_editor.forget();
}
}
fn show_errors() {
if let Ok(Some(header)) = document().query_selector("header") {
let list = document().create_element("header").unwrap();
list.class_list().add("messages").unwrap();
for error in document().query_selector_all("p.error").unwrap() {
let document = document();
if let Ok(Some(header)) = document.query_selector("header") {
let list = document.create_element("header").unwrap();
list.class_list().add_1("messages").unwrap();
let errors = document.query_selector_all("p.error").unwrap();
for i in 0..errors.length() {
let error = errors.get(i).unwrap();
error
.parent_element()
.unwrap()
.remove_child(&error)
.unwrap();
list.append_child(&error);
let _ = list.append_child(&error);
}
header
.parent_element()
.unwrap()
.insert_before(&list, &header.next_sibling().unwrap())
.insert_before(&list, header.next_sibling().as_ref())
.unwrap();
}
}
@@ -401,9 +496,17 @@ fn init_popup(
content: &HtmlElement,
old_ed: &Element,
) -> Result<Element, EditorError> {
let popup = document().create_element("div")?;
popup.class_list().add("popup")?;
popup.set_attribute("id", "publish-popup")?;
let document = document();
let popup = document
.create_element("div")
.map_err(|_| EditorError::DOMError)?;
popup
.class_list()
.add_1("popup")
.map_err(|_| EditorError::DOMError)?;
popup
.set_attribute("id", "publish-popup")
.map_err(|_| EditorError::DOMError)?;
let tags = get_elt_value("tags")
.split(',')
@@ -411,112 +514,165 @@ fn init_popup(
.map(str::to_string)
.collect::<Vec<_>>();
let license = get_elt_value("license");
make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_raw_value(&tags.join(", "));
make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_raw_value(&license);
make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_value(&tags.join(", "));
make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_value(&license);
let cover_label = document().create_element("label")?;
cover_label.append_child(&document().create_text_node(&i18n!(CATALOG, "Cover")));
cover_label.set_attribute("for", "cover")?;
let cover = document().get_element_by_id("cover")?;
cover.parent_element()?.remove_child(&cover).ok();
popup.append_child(&cover_label);
popup.append_child(&cover);
let cover_label = document
.create_element("label")
.map_err(|_| EditorError::DOMError)?;
cover_label
.append_child(&document.create_text_node(&i18n!(CATALOG, "Cover")))
.map_err(|_| EditorError::DOMError)?;
cover_label
.set_attribute("for", "cover")
.map_err(|_| EditorError::DOMError)?;
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)?;
popup
.append_child(&cover)
.map_err(|_| EditorError::DOMError)?;
if let Some(draft_checkbox) = document().get_element_by_id("draft") {
let draft_label = document().create_element("label")?;
draft_label.set_attribute("for", "popup-draft")?;
if let Some(draft_checkbox) = document.get_element_by_id("draft") {
let draft_checkbox = draft_checkbox.dyn_ref::<HtmlInputElement>().unwrap();
let draft_label = document
.create_element("label")
.map_err(|_| EditorError::DOMError)?;
draft_label
.set_attribute("for", "popup-draft")
.map_err(|_| EditorError::DOMError)?;
let draft = document().create_element("input").unwrap();
js! {
@{&draft}.id = "popup-draft";
@{&draft}.name = "popup-draft";
@{&draft}.type = "checkbox";
@{&draft}.checked = @{&draft_checkbox}.checked;
};
let draft = document.create_element("input").unwrap();
draft.set_id("popup-draft");
let draft = draft.dyn_ref::<HtmlInputElement>().unwrap();
draft.set_name("popup-draft");
draft.set_type("checkbox");
draft.set_checked(draft_checkbox.checked());
draft_label.append_child(&draft);
draft_label.append_child(&document().create_text_node(&i18n!(CATALOG, "This is a draft")));
popup.append_child(&draft_label);
draft_label
.append_child(draft)
.map_err(|_| EditorError::DOMError)?;
draft_label
.append_child(&document.create_text_node(&i18n!(CATALOG, "This is a draft")))
.map_err(|_| EditorError::DOMError)?;
popup
.append_child(&draft_label)
.map_err(|_| EditorError::DOMError)?;
}
let button = document().create_element("input")?;
js! {
@{&button}.type = "submit";
@{&button}.value = @{i18n!(CATALOG, "Publish")};
};
button.append_child(&document().create_text_node(&i18n!(CATALOG, "Publish")));
button.add_event_listener(
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
title.focus(); // Remove the placeholder before publishing
set_value("title", title.inner_text());
subtitle.focus();
set_value("subtitle", subtitle.inner_text());
content.focus();
set_value("editor-content", content.child_nodes().iter().fold(String::new(), |md, ch| {
let to_append = match ch.node_type() {
NodeType::Element => {
if js!{ return @{&ch}.tagName; } == "DIV" {
(js!{ return @{&ch}.innerHTML; }).try_into().unwrap_or_default()
} else {
(js!{ return @{&ch}.outerHTML; }).try_into().unwrap_or_default()
}
},
NodeType::Text => ch.node_value().unwrap_or_default(),
_ => unreachable!(),
};
format!("{}\n\n{}", md, to_append)
}));
set_value("tags", get_elt_value("popup-tags"));
if let Some(draft) = document().get_element_by_id("popup-draft") {
js!{
document.getElementById("draft").checked = @{draft}.checked;
};
}
let cover = document().get_element_by_id("cover").unwrap();
cover.parent_element().unwrap().remove_child(&cover).ok();
old_ed.append_child(&cover);
set_value("license", get_elt_value("popup-license"));
clear_autosave();
js! {
@{&old_ed}.submit();
let button = document
.create_element("input")
.map_err(|_| EditorError::DOMError)?;
button
.append_child(&document.create_text_node(&i18n!(CATALOG, "Publish")))
.map_err(|_| EditorError::DOMError)?;
let button = button.dyn_ref::<HtmlInputElement>().unwrap();
button.set_type("submit");
button.set_value(&i18n!(CATALOG, "Publish"));
let callback = Closure::wrap(Box::new(mv!(title, subtitle, content, old_ed => move |_| {
let document = self::document();
title.focus().unwrap(); // Remove the placeholder before publishing
set_value("title", title.inner_text());
subtitle.focus().unwrap();
set_value("subtitle", subtitle.inner_text());
content.focus().unwrap();
let mut md = String::new();
let child_nodes = content.child_nodes();
for i in 0..child_nodes.length() {
let ch = child_nodes.get(i).unwrap();
let to_append = match ch.node_type() {
Node::ELEMENT_NODE => {
let ch = ch.dyn_ref::<Element>().unwrap();
if ch.tag_name() == "DIV" {
ch.inner_html()
} else {
ch.outer_html()
}
},
Node::TEXT_NODE => ch.node_value().unwrap_or_default(),
_ => unreachable!(),
};
}),
);
popup.append_child(&button);
md = format!("{}\n\n{}", md, to_append);
}
set_value("editor-content", md);
set_value("tags", get_elt_value("popup-tags"));
if let Some(draft) = document.get_element_by_id("popup-draft") {
if let Some(draft_checkbox) = document.get_element_by_id("draft") {
let draft_checkbox = draft_checkbox.dyn_ref::<HtmlInputElement>().unwrap();
let draft = draft.dyn_ref::<HtmlInputElement>().unwrap();
draft_checkbox.set_checked(draft.checked());
}
}
let cover = document.get_element_by_id("cover").unwrap();
cover.parent_element().unwrap().remove_child(&cover).ok();
old_ed.append_child(&cover).unwrap();
set_value("license", get_elt_value("popup-license"));
clear_autosave();
let old_ed = old_ed.dyn_ref::<HtmlFormElement>().unwrap();
old_ed.submit().unwrap();
})) as Box<dyn FnMut(MouseEvent)>);
button
.add_event_listener_with_callback("click", callback.as_ref().unchecked_ref())
.map_err(|_| EditorError::DOMError)?;
callback.forget();
popup
.append_child(button)
.map_err(|_| EditorError::DOMError)?;
document().body()?.append_child(&popup);
document
.body()
.ok_or(EditorError::NoneError)?
.append_child(&popup)
.map_err(|_| EditorError::DOMError)?;
Ok(popup)
}
fn init_popup_bg() -> Result<Element, EditorError> {
let bg = document().create_element("div")?;
bg.class_list().add("popup-bg")?;
bg.set_attribute("id", "popup-bg")?;
let bg = document()
.create_element("div")
.map_err(|_| EditorError::DOMError)?;
bg.class_list()
.add_1("popup-bg")
.map_err(|_| EditorError::DOMError)?;
bg.set_attribute("id", "popup-bg")
.map_err(|_| EditorError::DOMError)?;
document().body()?.append_child(&bg);
bg.add_event_listener(|_: ClickEvent| close_popup());
document()
.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)>);
bg.add_event_listener_with_callback("click", callback.as_ref().unchecked_ref())
.unwrap();
callback.forget();
Ok(bg)
}
fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
match document().query_selector(selector) {
Ok(Some(form)) => HtmlElement::try_from(form).ok().and_then(|form| {
Ok(Some(form)) => form.dyn_ref::<HtmlElement>().and_then(|form| {
if let Some(len) = form
.get_attribute("content-size")
.and_then(|s| s.parse::<i32>().ok())
{
(js! {
let x = encodeURIComponent(@{content}.innerHTML)
.replace(/%20/g, "+")
.replace(/%0A/g, "%0D%0A")
.replace(new RegExp("[!'*()]", "g"), "XXX") // replace exceptions of encodeURIComponent with placeholder
.length + 2;
console.log(x);
return x;
})
.try_into()
.map(|c: i32| len - c)
.ok()
(encode_uri_component(&content.inner_html())
.replace("%20", "+")
.replace("%0A", "%0D0A")
.replace_by_pattern(&RegExp::new("[!'*()]", "g"), "XXX")
.length()
+ 2_u32)
.try_into()
.map(|c: i32| len - c)
.ok()
} else {
None
}
@@ -526,26 +682,26 @@ fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
}
fn close_popup() {
let hide = |x: Element| x.class_list().remove("show");
let hide = |x: Element| x.class_list().remove_1("show");
document().get_element_by_id("publish-popup").map(hide);
document().get_element_by_id("popup-bg").map(hide);
}
fn make_input(label_text: &str, name: &'static str, form: &Element) -> InputElement {
let label = document().create_element("label").unwrap();
label.append_child(&document().create_text_node(label_text));
fn make_input(label_text: &str, name: &'static str, form: &Element) -> HtmlInputElement {
let document = document();
let label = document.create_element("label").unwrap();
label
.append_child(&document.create_text_node(label_text))
.unwrap();
label.set_attribute("for", name).unwrap();
let inp: InputElement = document()
.create_element("input")
.unwrap()
.try_into()
.unwrap();
let inp = document.create_element("input").unwrap();
let inp = inp.dyn_into::<HtmlInputElement>().unwrap();
inp.set_attribute("name", name).unwrap();
inp.set_attribute("id", name).unwrap();
form.append_child(&label);
form.append_child(&inp);
form.append_child(&label).unwrap();
form.append_child(&inp).unwrap();
inp
}
@@ -559,36 +715,46 @@ fn make_editable(tag: &'static str) -> Element {
}
fn placeholder(elt: HtmlElement, text: &str) -> HtmlElement {
elt.dataset().insert("placeholder", text).unwrap();
elt.dataset().insert("edited", "false").unwrap();
elt.dataset().set("placeholder", text).unwrap();
elt.dataset().set("edited", "false").unwrap();
elt.add_event_listener(mv!(elt => move |_: FocusEvent| {
let callback = Closure::wrap(Box::new(mv!(elt => move |_: FocusEvent| {
if elt.dataset().get("edited").unwrap().as_str() != "true" {
clear_children(&elt);
}
}));
elt.add_event_listener(mv!(elt => move |_: BlurEvent| {
})) as Box<dyn FnMut(FocusEvent)>);
elt.add_event_listener_with_callback("focus", callback.as_ref().unchecked_ref())
.unwrap();
callback.forget();
let callback = Closure::wrap(Box::new(mv!(elt => move |_: Event| {
if elt.dataset().get("edited").unwrap().as_str() != "true" {
clear_children(&elt);
let ph = document().create_element("span").expect("Couldn't create placeholder");
ph.class_list().add("placeholder").expect("Couldn't add class");
ph.append_child(&document().create_text_node(&elt.dataset().get("placeholder").unwrap_or_default()));
elt.append_child(&ph);
ph.class_list().add_1("placeholder").expect("Couldn't add class");
ph.append_child(&document().create_text_node(&elt.dataset().get("placeholder").unwrap_or_default())).unwrap();
elt.append_child(&ph).unwrap();
}
}));
elt.add_event_listener(mv!(elt => move |_: KeyUpEvent| {
elt.dataset().insert("edited", if elt.inner_text().trim_matches('\n').is_empty() {
})) as Box<dyn FnMut(Event)>);
elt.add_event_listener_with_callback("blur", callback.as_ref().unchecked_ref())
.unwrap();
callback.forget();
let callback = Closure::wrap(Box::new(mv!(elt => move |_: KeyboardEvent| {
elt.dataset().set("edited", if elt.inner_text().trim_matches('\n').is_empty() {
"false"
} else {
"true"
}).expect("Couldn't update edition state");
}));
})) as Box<dyn FnMut(KeyboardEvent)>);
elt.add_event_listener_with_callback("keyup", callback.as_ref().unchecked_ref())
.unwrap();
callback.forget();
elt
}
fn clear_children(elt: &HtmlElement) {
for child in elt.child_nodes() {
elt.remove_child(&child).unwrap();
let child_nodes = elt.child_nodes();
for _ in 0..child_nodes.length() {
elt.remove_child(&child_nodes.get(0).unwrap()).unwrap();
}
}
+174
View File
@@ -0,0 +1,174 @@
#![recursion_limit = "128"]
#![feature(decl_macro, proc_macro_hygiene)]
#[macro_use]
extern crate gettext_macros;
#[macro_use]
extern crate lazy_static;
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{console, window, Document, Element, Event, HtmlInputElement, TouchEvent};
init_i18n!(
"plume-front",
af,
ar,
bg,
ca,
cs,
cy,
da,
de,
el,
en,
eo,
es,
fa,
fi,
fr,
gl,
he,
hi,
hr,
hu,
it,
ja,
ko,
nb,
nl,
no,
pl,
pt,
ro,
ru,
sat,
si,
sk,
sl,
sr,
sv,
tr,
uk,
vi,
zh
);
mod editor;
compile_i18n!();
lazy_static! {
static ref CATALOG: gettext::Catalog = {
let catalogs = include_i18n!();
let lang = window().unwrap().navigator().language().unwrap();
let lang = lang.split_once('-').map_or("en", |x| x.0);
let english_position = catalogs
.iter()
.position(|(language_code, _)| *language_code == "en")
.unwrap();
catalogs
.iter()
.find(|(l, _)| l == &lang)
.unwrap_or(&catalogs[english_position])
.clone()
.1
};
}
#[wasm_bindgen(start)]
pub fn main() -> Result<(), JsValue> {
extern crate console_error_panic_hook;
use std::panic;
panic::set_hook(Box::new(console_error_panic_hook::hook));
menu();
search();
editor::init()
.map_err(|e| console::error_1(&format!("Editor error: {:?}", e).into()))
.ok();
Ok(())
}
/// Toggle menu on mobile devices
///
/// It should normally be working fine even without this code
/// But :focus-within is not yet supported by Webkit/Blink
fn menu() {
let document = document();
if let Ok(Some(button)) = document.query_selector("#menu a") {
if let Some(menu) = document.get_element_by_id("content") {
let show_menu = Closure::wrap(Box::new(|_: TouchEvent| {
self::document()
.get_element_by_id("menu")
.map(|menu| {
menu.set_attribute("aria-expanded", "true")
.map(|_| menu.class_list().add_1("show"))
})
.unwrap()
.unwrap()
.unwrap();
}) as Box<dyn FnMut(TouchEvent)>);
button
.add_event_listener_with_callback("touchend", show_menu.as_ref().unchecked_ref())
.unwrap();
show_menu.forget();
let close_menu = Closure::wrap(Box::new(|evt: TouchEvent| {
if evt
.target()
.unwrap()
.dyn_ref::<Element>()
.unwrap()
.closest("a")
.unwrap()
.is_some()
{
return;
}
self::document()
.get_element_by_id("menu")
.map(|menu| {
menu.set_attribute("aria-expanded", "false")
.map(|_| menu.class_list().remove_1("show"))
})
.unwrap()
.unwrap()
.unwrap()
}) as Box<dyn FnMut(TouchEvent)>);
menu.add_event_listener_with_callback("touchend", close_menu.as_ref().unchecked_ref())
.unwrap();
close_menu.forget();
}
}
}
/// Clear the URL of the search page before submitting request
fn search() {
if let Some(form) = document().get_element_by_id("form") {
let normalize_query = Closure::wrap(Box::new(|_: Event| {
document()
.query_selector_all("#form input")
.map(|inputs| {
for i in 0..inputs.length() {
let input = inputs.get(i).unwrap();
let input = input.dyn_ref::<HtmlInputElement>().unwrap();
if input.name().is_empty() {
input.set_name(&input.dyn_ref::<Element>().unwrap().id());
}
if !input.name().is_empty() && input.value().is_empty() {
input.set_name("");
}
}
})
.unwrap();
}) as Box<dyn FnMut(Event)>);
form.add_event_listener_with_callback("submit", normalize_query.as_ref().unchecked_ref())
.unwrap();
normalize_query.forget();
}
}
fn document() -> Document {
window().unwrap().document().unwrap()
}
-112
View File
@@ -1,112 +0,0 @@
#![recursion_limit = "128"]
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
#[macro_use]
extern crate gettext_macros;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate stdweb;
use stdweb::web::{event::*, *};
init_i18n!(
"plume-front",
ar,
bg,
ca,
cs,
de,
en,
eo,
es,
fa,
fr,
gl,
hi,
hr,
it,
ja,
nb,
pl,
pt,
ro,
ru,
sr,
sk,
sv
);
mod editor;
compile_i18n!();
lazy_static! {
static ref CATALOG: gettext::Catalog = {
let catalogs = include_i18n!();
let lang = js! { return navigator.language }.into_string().unwrap();
let lang = lang.splitn(2, '-').next().unwrap_or("en");
let english_position = catalogs
.iter()
.position(|(language_code, _)| *language_code == "en")
.unwrap();
catalogs
.iter()
.find(|(l, _)| l == &lang)
.unwrap_or(&catalogs[english_position])
.clone()
.1
};
}
fn main() {
menu();
search();
editor::init()
.map_err(|e| console!(error, format!("Editor error: {:?}", e)))
.ok();
}
/// Toggle menu on mobile devices
///
/// It should normally be working fine even without this code
/// But :focus-within is not yet supported by Webkit/Blink
fn menu() {
if let Some(button) = document().get_element_by_id("menu") {
if let Some(menu) = document().get_element_by_id("content") {
button.add_event_listener(|_: TouchEnd| {
document()
.get_element_by_id("menu")
.map(|menu| menu.class_list().add("show"));
});
menu.add_event_listener(|_: TouchEnd| {
document()
.get_element_by_id("menu")
.map(|menu| menu.class_list().remove("show"));
});
}
}
}
/// Clear the URL of the search page before submitting request
fn search() {
if let Some(form) = document().get_element_by_id("form") {
form.add_event_listener(|_: SubmitEvent| {
document()
.query_selector_all("#form input")
.map(|inputs| {
for input in inputs {
js! {
if (@{&input}.name === "") {
@{&input}.name = @{&input}.id
}
if (@{&input}.name && !@{&input}.value) {
@{&input}.name = "";
}
}
}
})
.ok();
});
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "plume-macro"
version = "0.6.0"
version = "0.7.1"
authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"]
edition = "2018"
description = "Plume procedural macros"
Regular → Executable
+5 -5
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())
@@ -89,12 +89,12 @@ fn file_to_migration(file: &str) -> TokenStream2 {
let mut actions = vec![];
for line in file.lines() {
if sql {
if line.starts_with("--#!") {
if let Some(acc_str) = line.strip_prefix("--#!") {
if !acc.trim().is_empty() {
actions.push(quote!(Action::Sql(#acc)));
}
sql = false;
acc = line[4..].to_string();
acc = acc_str.to_string();
acc.push('\n');
} else if line.starts_with("--") {
continue;
@@ -102,8 +102,8 @@ fn file_to_migration(file: &str) -> TokenStream2 {
acc.push_str(line);
acc.push('\n');
}
} else if line.starts_with("--#!") {
acc.push_str(&line[4..]);
} else if let Some(acc_str) = line.strip_prefix("--#!") {
acc.push_str(acc_str);
acc.push('\n');
} else if line.starts_with("--") {
continue;
+18 -15
View File
@@ -1,37 +1,40 @@
[package]
name = "plume-models"
version = "0.6.0"
version = "0.7.1"
authors = ["Plume contributors"]
edition = "2018"
[dependencies]
activitypub = "0.1.1"
ammonia = "2.1.1"
askama_escape = "0.1"
bcrypt = "0.5"
guid-create = "0.1"
heck = "0.3.0"
itertools = "0.8.0"
ammonia = "3.1.2"
bcrypt = "0.10.1"
guid-create = "0.2"
itertools = "0.10.3"
lazy_static = "1.0"
ldap3 = "0.7.1"
ldap3 = "0.9.3"
migrations_internals= "1.4.0"
openssl = "0.10.22"
rocket = "0.4.5"
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"
tantivy = "0.12.0"
serde_json = "1.0.70"
tantivy = "0.13.3"
url = "2.1"
walkdir = "2.2"
webfinger = "0.4.1"
whatlang = "0.7.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.1.3", optional = true }
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"]
@@ -39,7 +42,7 @@ version = "0.4"
[dependencies.diesel]
features = ["r2d2", "chrono"]
version = "1.4.2"
version = "1.4.5"
[dependencies.plume-api]
path = "../plume-api"
Binary file not shown.
Binary file not shown.
+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
+7 -8
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)
@@ -87,7 +87,7 @@ impl BlocklistedEmail {
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::{instance::tests as instance_tests, tests::rockets, Connection as Conn};
use crate::{instance::tests as instance_tests, tests::db, Connection as Conn};
use diesel::Connection;
pub(crate) fn fill_database(conn: &Conn) -> Vec<BlocklistedEmail> {
@@ -106,29 +106,28 @@ pub(crate) mod tests {
}
#[test]
fn test_match() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let various = fill_database(conn);
let various = fill_database(&conn);
let match1 = "user1@bad-actor.com";
let match2 = "spammer@lax-administration.com";
let no_match = "happy-user@lax-administration.com";
assert_eq!(
BlocklistedEmail::matches_blocklist(conn, match1)
BlocklistedEmail::matches_blocklist(&conn, match1)
.unwrap()
.unwrap()
.id,
various[0].id
);
assert_eq!(
BlocklistedEmail::matches_blocklist(conn, match2)
BlocklistedEmail::matches_blocklist(&conn, match2)
.unwrap()
.unwrap()
.id,
various[1].id
);
assert_eq!(
BlocklistedEmail::matches_blocklist(conn, no_match)
BlocklistedEmail::matches_blocklist(&conn, no_match)
.unwrap()
.is_none(),
true
+100 -99
View File
@@ -1,6 +1,6 @@
use crate::{
ap_url, instance::*, medias::Media, posts::Post, safe_string::SafeString, schema::blogs,
search::Searcher, users::User, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE,
ap_url, db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString,
schema::blogs, users::User, Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
};
use activitypub::{
actor::Group,
@@ -20,7 +20,6 @@ use plume_common::activity_pub::{
inbox::{AsActor, FromId},
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
};
use serde_json;
use url::Url;
use webfinger::*;
@@ -132,25 +131,33 @@ impl Blog {
.map_err(Error::from)
}
pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<Blog> {
pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result<Blog> {
let from_db = blogs::table
.filter(blogs::fqn.eq(fqn))
.first(&*c.conn)
.first(&**conn)
.optional()?;
if let Some(from_db) = from_db {
Ok(from_db)
} else {
Blog::fetch_from_webfinger(c, fqn)
Blog::fetch_from_webfinger(conn, fqn)
}
}
fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<Blog> {
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<Blog> {
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
.links
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger)
.and_then(|l| Blog::from_id(c, &l.href?, None).map_err(|(_, e)| e))
.and_then(|l| {
Blog::from_id(
conn,
&l.href.ok_or(Error::MissingApProperty)?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)
})
}
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
@@ -217,16 +224,16 @@ impl Blog {
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
let mut coll = OrderedCollection::default();
coll.collection_props.items = serde_json::to_value(self.get_activities(conn)?)?;
coll.collection_props.items = serde_json::to_value(self.get_activities(conn))?;
coll.collection_props
.set_total_items_u64(self.get_activities(conn)?.len() as u64)?;
.set_total_items_u64(self.get_activities(conn).len() as u64)?;
coll.collection_props
.set_first_link(Id::new(ap_url(&format!("{}?page=1", &self.outbox_url))))?;
coll.collection_props
.set_last_link(Id::new(ap_url(&format!(
"{}?page={}",
&self.outbox_url,
(self.get_activities(conn)?.len() as u64 + ITEMS_PER_PAGE as u64 - 1) as u64
(self.get_activities(conn).len() as u64 + ITEMS_PER_PAGE as u64 - 1) as u64
/ ITEMS_PER_PAGE as u64
))))?;
Ok(ActivityStream::new(coll))
@@ -237,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!(
@@ -253,20 +260,23 @@ impl Blog {
coll.collection_props.items = serde_json::to_value(acts)?;
Ok(ActivityStream::new(coll))
}
fn get_activities(&self, _conn: &Connection) -> Result<Vec<serde_json::Value>> {
Ok(vec![])
fn get_activities(&self, _conn: &Connection) -> Vec<serde_json::Value> {
vec![]
}
fn get_activity_page(
&self,
_conn: &Connection,
(_min, _max): (i32, i32),
) -> Result<Vec<serde_json::Value>> {
Ok(vec![])
) -> Vec<serde_json::Value> {
vec![]
}
pub fn get_keypair(&self) -> Result<PKey<Private>> {
PKey::from_rsa(Rsa::private_key_from_pem(
self.private_key.clone()?.as_ref(),
self.private_key
.clone()
.ok_or(Error::MissingApProperty)?
.as_ref(),
)?)
.map_err(Error::from)
}
@@ -318,9 +328,9 @@ impl Blog {
.and_then(|c| c.url().ok())
}
pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
for post in Post::get_for_blog(conn, &self)? {
post.delete(conn, searcher)?;
pub fn delete(&self, conn: &Connection) -> Result<()> {
for post in Post::get_for_blog(conn, self)? {
post.delete(conn)?;
}
diesel::delete(self)
.execute(conn)
@@ -335,20 +345,20 @@ impl IntoId for Blog {
}
}
impl FromId<PlumeRocket> for Blog {
impl FromId<DbConn> for Blog {
type Error = Error;
type Object = CustomGroup;
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Self::find_by_ap_url(&c.conn, id)
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id)
}
fn from_activity(c: &PlumeRocket, acct: CustomGroup) -> Result<Self> {
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 instance = Instance::find_by_domain(&c.conn, inst).or_else(|_| {
let inst = url.host_str().ok_or(Error::Url)?;
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
Instance::insert(
&c.conn,
conn,
NewInstance {
public_domain: inst.to_owned(),
name: inst.to_owned(),
@@ -371,9 +381,9 @@ impl FromId<PlumeRocket> for Blog {
.and_then(|icon| {
let owner = icon.object_props.attributed_to_link::<Id>().ok()?;
Media::save_remote(
&c.conn,
conn,
icon.object_props.url_string().ok()?,
&User::from_id(c, &owner, None).ok()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
)
.ok()
})
@@ -387,9 +397,9 @@ impl FromId<PlumeRocket> for Blog {
.and_then(|banner| {
let owner = banner.object_props.attributed_to_link::<Id>().ok()?;
Media::save_remote(
&c.conn,
conn,
banner.object_props.url_string().ok()?,
&User::from_id(c, &owner, None).ok()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
)
.ok()
})
@@ -401,7 +411,7 @@ impl FromId<PlumeRocket> for Blog {
}
Blog::insert(
&c.conn,
conn,
NewBlog {
actor_id: name.clone(),
title: acct.object.object_props.name_string().unwrap_or(name),
@@ -433,6 +443,10 @@ impl FromId<PlumeRocket> 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 {
@@ -452,24 +466,22 @@ impl AsActor<&PlumeRocket> for Blog {
}
impl sign::Signer for Blog {
type Error = Error;
fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url)
}
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = self.get_keypair()?;
fn sign(&self, to_sign: &str) -> sign::Result<Vec<u8>> {
let key = self.get_keypair().map_err(|_| sign::Error())?;
let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?;
signer.sign_to_vec().map_err(Error::from)
signer.sign_to_vec().map_err(sign::Error::from)
}
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
fn verify(&self, data: &str, signature: &[u8]) -> sign::Result<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?;
verifier.verify(&signature).map_err(Error::from)
verifier.verify(signature).map_err(sign::Error::from)
}
}
@@ -497,14 +509,8 @@ impl NewBlog {
pub(crate) mod tests {
use super::*;
use crate::{
blog_authors::*,
config::CONFIG,
instance::tests as instance_tests,
medias::NewMedia,
search::tests::get_searcher,
tests::{db, rockets},
users::tests as usersTests,
Connection as Conn,
blog_authors::*, instance::tests as instance_tests, medias::NewMedia, tests::db,
users::tests as usersTests, Connection as Conn,
};
use diesel::Connection;
@@ -591,10 +597,10 @@ pub(crate) mod tests {
fn get_instance() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(conn);
fill_database(&conn);
let blog = Blog::insert(
conn,
&conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
@@ -606,7 +612,7 @@ pub(crate) mod tests {
.unwrap();
assert_eq!(
blog.get_instance(conn).unwrap().id,
blog.get_instance(&conn).unwrap().id,
Instance::get_local().unwrap().id
);
// TODO add tests for remote instance
@@ -618,10 +624,10 @@ pub(crate) mod tests {
fn authors() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (user, _) = fill_database(conn);
let (user, _) = fill_database(&conn);
let b1 = Blog::insert(
conn,
&conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
@@ -632,7 +638,7 @@ pub(crate) mod tests {
)
.unwrap();
let b2 = Blog::insert(
conn,
&conn,
NewBlog::new_local(
"Blog".to_owned(),
"Blog".to_owned(),
@@ -645,7 +651,7 @@ pub(crate) mod tests {
let blog = vec![b1, b2];
BlogAuthor::insert(
conn,
&conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[0].id,
@@ -655,7 +661,7 @@ pub(crate) mod tests {
.unwrap();
BlogAuthor::insert(
conn,
&conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[1].id,
@@ -665,7 +671,7 @@ pub(crate) mod tests {
.unwrap();
BlogAuthor::insert(
conn,
&conn,
NewBlogAuthor {
blog_id: blog[1].id,
author_id: user[0].id,
@@ -675,39 +681,39 @@ pub(crate) mod tests {
.unwrap();
assert!(blog[0]
.list_authors(conn)
.list_authors(&conn)
.unwrap()
.iter()
.any(|a| a.id == user[0].id));
assert!(blog[0]
.list_authors(conn)
.list_authors(&conn)
.unwrap()
.iter()
.any(|a| a.id == user[1].id));
assert!(blog[1]
.list_authors(conn)
.list_authors(&conn)
.unwrap()
.iter()
.any(|a| a.id == user[0].id));
assert!(!blog[1]
.list_authors(conn)
.list_authors(&conn)
.unwrap()
.iter()
.any(|a| a.id == user[1].id));
assert!(Blog::find_for_author(conn, &user[0])
assert!(Blog::find_for_author(&conn, &user[0])
.unwrap()
.iter()
.any(|b| b.id == blog[0].id));
assert!(Blog::find_for_author(conn, &user[1])
assert!(Blog::find_for_author(&conn, &user[1])
.unwrap()
.iter()
.any(|b| b.id == blog[0].id));
assert!(Blog::find_for_author(conn, &user[0])
assert!(Blog::find_for_author(&conn, &user[0])
.unwrap()
.iter()
.any(|b| b.id == blog[1].id));
assert!(!Blog::find_for_author(conn, &user[1])
assert!(!Blog::find_for_author(&conn, &user[1])
.unwrap()
.iter()
.any(|b| b.id == blog[1].id));
@@ -717,13 +723,12 @@ pub(crate) mod tests {
#[test]
fn find_local() {
let r = rockets();
let conn = &*r.conn;
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(conn);
fill_database(&conn);
let blog = Blog::insert(
conn,
&conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
@@ -734,7 +739,7 @@ pub(crate) mod tests {
)
.unwrap();
assert_eq!(Blog::find_by_fqn(&r, "SomeName").unwrap().id, blog.id);
assert_eq!(Blog::find_by_fqn(&conn, "SomeName").unwrap().id, blog.id);
Ok(())
})
}
@@ -743,10 +748,10 @@ pub(crate) mod tests {
fn get_fqn() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(conn);
fill_database(&conn);
let blog = Blog::insert(
conn,
&conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
@@ -766,12 +771,10 @@ pub(crate) mod tests {
fn delete() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (_, blogs) = fill_database(conn);
let (_, blogs) = fill_database(&conn);
blogs[0]
.delete(conn, &get_searcher(&CONFIG.search_tokenizers))
.unwrap();
assert!(Blog::get(conn, blogs[0].id).is_err());
blogs[0].delete(&conn).unwrap();
assert!(Blog::get(&conn, blogs[0].id).is_err());
Ok(())
})
}
@@ -780,11 +783,10 @@ pub(crate) mod tests {
fn delete_via_user() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let searcher = get_searcher(&CONFIG.search_tokenizers);
let (user, _) = fill_database(conn);
let (user, _) = fill_database(&conn);
let b1 = Blog::insert(
conn,
&conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
@@ -795,7 +797,7 @@ pub(crate) mod tests {
)
.unwrap();
let b2 = Blog::insert(
conn,
&conn,
NewBlog::new_local(
"Blog".to_owned(),
"Blog".to_owned(),
@@ -808,7 +810,7 @@ pub(crate) mod tests {
let blog = vec![b1, b2];
BlogAuthor::insert(
conn,
&conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[0].id,
@@ -818,7 +820,7 @@ pub(crate) mod tests {
.unwrap();
BlogAuthor::insert(
conn,
&conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[1].id,
@@ -828,7 +830,7 @@ pub(crate) mod tests {
.unwrap();
BlogAuthor::insert(
conn,
&conn,
NewBlogAuthor {
blog_id: blog[1].id,
author_id: user[0].id,
@@ -837,24 +839,23 @@ pub(crate) mod tests {
)
.unwrap();
user[0].delete(conn, &searcher).unwrap();
assert!(Blog::get(conn, blog[0].id).is_ok());
assert!(Blog::get(conn, blog[1].id).is_err());
user[1].delete(conn, &searcher).unwrap();
assert!(Blog::get(conn, blog[0].id).is_err());
user[0].delete(&conn).unwrap();
assert!(Blog::get(&conn, blog[0].id).is_ok());
assert!(Blog::get(&conn, blog[1].id).is_err());
user[1].delete(&conn).unwrap();
assert!(Blog::get(&conn, blog[0].id).is_err());
Ok(())
})
}
#[test]
fn self_federation() {
let r = rockets();
let conn = &*r.conn;
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, mut blogs) = fill_database(conn);
let (users, mut blogs) = fill_database(&conn);
blogs[0].icon_id = Some(
Media::insert(
conn,
&conn,
NewMedia {
file_path: "aaa.png".into(),
alt_text: String::new(),
@@ -870,7 +871,7 @@ pub(crate) mod tests {
);
blogs[0].banner_id = Some(
Media::insert(
conn,
&conn,
NewMedia {
file_path: "bbb.png".into(),
alt_text: String::new(),
@@ -884,11 +885,11 @@ pub(crate) mod tests {
.unwrap()
.id,
);
let _: Blog = blogs[0].save_changes(conn).unwrap();
let _: Blog = blogs[0].save_changes(&**conn).unwrap();
let ap_repr = blogs[0].to_activity(conn).unwrap();
blogs[0].delete(conn, &*r.searcher).unwrap();
let blog = Blog::from_activity(&r, ap_repr).unwrap();
let ap_repr = blogs[0].to_activity(&conn).unwrap();
blogs[0].delete(&conn).unwrap();
let blog = Blog::from_activity(&conn, ap_repr).unwrap();
assert_eq!(blog.actor_id, blogs[0].actor_id);
assert_eq!(blog.title, blogs[0].title);
@@ -900,8 +901,8 @@ pub(crate) mod tests {
assert_eq!(blog.public_key, blogs[0].public_key);
assert_eq!(blog.fqn, blogs[0].fqn);
assert_eq!(blog.summary_html, blogs[0].summary_html);
assert_eq!(blog.icon_url(conn), blogs[0].icon_url(conn));
assert_eq!(blog.banner_url(conn), blogs[0].banner_url(conn));
assert_eq!(blog.icon_url(&conn), blogs[0].icon_url(&conn));
assert_eq!(blog.banner_url(&conn), blogs[0].banner_url(&conn));
Ok(())
})
+58 -46
View File
@@ -1,5 +1,6 @@
use crate::{
comment_seers::{CommentSeers, NewCommentSeers},
db_conn::DbConn,
instance::Instance,
medias::Media,
mentions::Mention,
@@ -8,7 +9,7 @@ use crate::{
safe_string::SafeString,
schema::comments,
users::User,
Connection, Error, PlumeRocket, Result,
Connection, Error, Result, CONFIG,
};
use activitypub::{
activity::{Create, Delete},
@@ -20,11 +21,11 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use plume_common::{
activity_pub::{
inbox::{AsActor, AsObject, FromId},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY,
},
utils,
};
use serde_json;
use std::collections::HashSet;
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
@@ -105,13 +106,13 @@ impl Comment {
.unwrap_or(false)
}
pub fn to_activity(&self, c: &PlumeRocket) -> Result<Note> {
let author = User::get(&c.conn, self.author_id)?;
pub fn to_activity(&self, conn: &DbConn) -> Result<Note> {
let author = User::get(conn, self.author_id)?;
let (html, mentions, _hashtags) = utils::md_to_html(
self.content.get().as_ref(),
Some(&Instance::get_local()?.public_domain),
true,
Some(Media::get_media_processor(&c.conn, vec![&author])),
Some(Media::get_media_processor(conn, vec![&author])),
);
let mut note = Note::default();
@@ -124,8 +125,8 @@ impl Comment {
note.object_props.set_content_string(html)?;
note.object_props
.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(
|| Ok(Post::get(&c.conn, self.post_id)?.ap_url),
|id| Ok(Comment::get(&c.conn, id)?.ap_url.unwrap_or_default()) as Result<String>,
|| Ok(Post::get(conn, self.post_id)?.ap_url),
|id| Ok(Comment::get(conn, id)?.ap_url.unwrap_or_default()) as Result<String>,
)?))?;
note.object_props
.set_published_string(chrono::Utc::now().to_rfc3339())?;
@@ -134,25 +135,27 @@ impl Comment {
note.object_props.set_tag_link_vec(
mentions
.into_iter()
.filter_map(|m| Mention::build_activity(c, &m).ok())
.filter_map(|m| Mention::build_activity(conn, &m).ok())
.collect::<Vec<link::Mention>>(),
)?;
Ok(note)
}
pub fn create_activity(&self, c: &PlumeRocket) -> Result<Create> {
let author = User::get(&c.conn, self.author_id)?;
pub fn create_activity(&self, conn: &DbConn) -> Result<Create> {
let author = User::get(conn, self.author_id)?;
let note = self.to_activity(c)?;
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(&c.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
@@ -194,18 +199,23 @@ impl Comment {
}
}
impl FromId<PlumeRocket> for Comment {
impl FromId<DbConn> for Comment {
type Error = Error;
type Object = Note;
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Self::find_by_ap_url(&c.conn, id)
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id)
}
fn from_activity(c: &PlumeRocket, note: Note) -> Result<Self> {
let conn = &*c.conn;
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,9 +246,10 @@ impl FromId<PlumeRocket> for Comment {
Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>
})?,
author_id: User::from_id(
c,
conn,
&note.object_props.attributed_to_link::<Id>()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
@@ -255,9 +266,7 @@ impl FromId<PlumeRocket> for Comment {
.and_then(|m| {
let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0];
let not_author = m.link_props.href_string()? != author.ap_url.clone();
Ok(Mention::from_activity(
conn, &m, comm.id, false, not_author,
)?)
Mention::from_activity(conn, &m, comm.id, false, not_author)
})
.ok();
}
@@ -296,7 +305,7 @@ impl FromId<PlumeRocket> 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(c, &v, None) {
if let Ok(user) = User::from_id(conn, &v, None, CONFIG.proxy()) {
vec![user]
} else {
vec![] // TODO try to fetch collection
@@ -320,43 +329,47 @@ impl FromId<PlumeRocket> 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, &PlumeRocket> for Comment {
impl AsObject<User, Create, &DbConn> for Comment {
type Error = Error;
type Output = Self;
fn activity(self, _c: &PlumeRocket, _actor: User, _id: &str) -> Result<Self> {
fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Self> {
// The actual creation takes place in the FromId impl
Ok(self)
}
}
impl AsObject<User, Delete, &PlumeRocket> for Comment {
impl AsObject<User, Delete, &DbConn> for Comment {
type Error = Error;
type Output = ();
fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
if self.author_id != actor.id {
return Err(Error::Unauthorized);
}
for m in Mention::list_for_comment(&c.conn, self.id)? {
for n in Notification::find_for_mention(&c.conn, &m)? {
n.delete(&c.conn)?;
for m in Mention::list_for_comment(conn, self.id)? {
for n in Notification::find_for_mention(conn, &m)? {
n.delete(conn)?;
}
m.delete(&c.conn)?;
m.delete(conn)?;
}
for n in Notification::find_for_comment(&c.conn, &self)? {
n.delete(&c.conn)?;
for n in Notification::find_for_comment(conn, &self)? {
n.delete(&**conn)?;
}
diesel::update(comments::table)
.filter(comments::in_response_to_id.eq(self.id))
.set(comments::in_response_to_id.eq(self.in_response_to_id))
.execute(&*c.conn)?;
diesel::delete(&self).execute(&*c.conn)?;
.execute(&**conn)?;
diesel::delete(&self).execute(&**conn)?;
Ok(())
}
}
@@ -392,17 +405,16 @@ mod tests {
use super::*;
use crate::inbox::{inbox, tests::fill_database, InboxResult};
use crate::safe_string::SafeString;
use crate::tests::rockets;
use crate::tests::db;
use diesel::Connection;
// creates a post, get it's Create activity, delete the post,
// "send" the Create to the inbox, and check it works
#[test]
fn self_federation() {
let r = rockets();
let conn = &*r.conn;
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&r);
let (posts, users, _) = fill_database(&conn);
let original_comm = Comment::insert(
conn,
@@ -418,14 +430,14 @@ mod tests {
},
)
.unwrap();
let act = original_comm.create_activity(&r).unwrap();
let act = original_comm.create_activity(&conn).unwrap();
inbox(
&r,
serde_json::to_value(original_comm.build_delete(conn).unwrap()).unwrap(),
&conn,
serde_json::to_value(original_comm.build_delete(&conn).unwrap()).unwrap(),
)
.unwrap();
match inbox(&r, serde_json::to_value(act).unwrap()).unwrap() {
match inbox(&conn, serde_json::to_value(act).unwrap()).unwrap() {
InboxResult::Commented(c) => {
// TODO: one is HTML, the other markdown: assert_eq!(c.content, original_comm.content);
assert_eq!(c.in_response_to_id, original_comm.in_response_to_id);
+91 -16
View File
@@ -1,6 +1,9 @@
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;
use std::env::{self, var};
#[cfg(not(test))]
@@ -14,31 +17,39 @@ 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>,
}
impl Config {
pub fn proxy(&self) -> Option<&reqwest::Proxy> {
self.proxy.as_ref().map(|p| &p.proxy)
}
}
#[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>()
@@ -49,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()
@@ -148,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()),
@@ -157,11 +168,8 @@ impl Default for LogoConfig {
};
let mut custom_icons = env::vars()
.filter_map(|(var, val)| {
if var.starts_with("PLUME_LOGO_") {
Some((var[11..].to_owned(), val))
} else {
None
}
var.strip_prefix("PLUME_LOGO_")
.map(|size| (size.to_owned(), val))
})
.filter_map(|(var, val)| var.parse::<u64>().ok().map(|var| (var, val)))
.map(|(dim, src)| Icon {
@@ -241,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,
@@ -277,6 +310,45 @@ fn get_ldap_config() -> Option<LdapConfig> {
}
}
pub struct ProxyConfig {
pub url: reqwest::Url,
pub only_domains: Option<HashSet<String>>,
pub proxy: reqwest::Proxy,
}
fn get_proxy_config() -> Option<ProxyConfig> {
let url: reqwest::Url = var("PROXY_URL").ok()?.parse().expect("Invalid PROXY_URL");
let proxy_url = url.clone();
let only_domains: Option<HashSet<String>> = var("PROXY_DOMAINS")
.ok()
.map(|ods| ods.split(',').map(str::to_owned).collect());
let proxy = if let Some(ref only_domains) = only_domains {
let only_domains = only_domains.clone();
reqwest::Proxy::custom(move |url| {
if let Some(domain) = url.domain() {
if only_domains.contains(domain)
|| only_domains
.iter()
.any(|target| domain.ends_with(&format!(".{}", target)))
{
Some(proxy_url.clone())
} else {
None
}
} else {
None
}
})
} else {
reqwest::Proxy::all(proxy_url).expect("Invalid PROXY_URL")
};
Some(ProxyConfig {
url,
only_domains,
proxy,
})
}
lazy_static! {
pub static ref CONFIG: Config = Config {
base_url: var("BASE_URL").unwrap_or_else(|_| format!(
@@ -292,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)),
@@ -304,6 +377,8 @@ 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()
}
}
+39 -20
View File
@@ -1,6 +1,6 @@
use crate::{
ap_url, notifications::*, schema::follows, users::User, Connection, Error, PlumeRocket, Result,
CONFIG,
ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User,
Connection, Error, Result, CONFIG,
};
use activitypub::activity::{Accept, Follow as FollowAct, Undo};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
@@ -116,7 +116,12 @@ impl Follow {
.accept_props
.set_actor_link::<Id>(target.clone().into_id())?;
accept.accept_props.set_object_object(follow)?;
broadcast(&*target, accept, vec![from.clone()]);
broadcast(
&*target,
accept,
vec![from.clone()],
CONFIG.proxy().cloned(),
);
Ok(res)
}
@@ -136,11 +141,11 @@ impl Follow {
}
}
impl AsObject<User, FollowAct, &PlumeRocket> for User {
impl AsObject<User, FollowAct, &DbConn> for User {
type Error = Error;
type Output = Follow;
fn activity(self, c: &PlumeRocket, actor: User, id: &str) -> Result<Follow> {
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Follow> {
// Mastodon (at least) requires the full Follow object when accepting it,
// so we rebuilt it here
let mut follow = FollowAct::default();
@@ -148,40 +153,54 @@ impl AsObject<User, FollowAct, &PlumeRocket> for User {
follow
.follow_props
.set_actor_link::<Id>(actor.clone().into_id())?;
Follow::accept_follow(&c.conn, &actor, &self, follow, actor.id, self.id)
Follow::accept_follow(conn, &actor, &self, follow, actor.id, self.id)
}
}
impl FromId<PlumeRocket> for Follow {
impl FromId<DbConn> for Follow {
type Error = Error;
type Object = FollowAct;
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Follow::find_by_ap_url(&c.conn, id)
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Follow::find_by_ap_url(conn, id)
}
fn from_activity(c: &PlumeRocket, follow: FollowAct) -> Result<Self> {
let actor =
User::from_id(c, &follow.follow_props.actor_link::<Id>()?, None).map_err(|(_, e)| e)?;
fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> {
let actor = User::from_id(
conn,
&follow.follow_props.actor_link::<Id>()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?;
let target = User::from_id(c, &follow.follow_props.object_link::<Id>()?, None)
.map_err(|(_, e)| e)?;
Follow::accept_follow(&c.conn, &actor, &target, follow, actor.id, target.id)
let target = User::from_id(
conn,
&follow.follow_props.object_link::<Id>()?,
None,
CONFIG.proxy(),
)
.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, &PlumeRocket> for Follow {
impl AsObject<User, Undo, &DbConn> for Follow {
type Error = Error;
type Output = ();
fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
let conn = &*c.conn;
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
let conn = conn;
if self.follower_id == actor.id {
diesel::delete(&self).execute(conn)?;
diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
diesel::delete(&notif).execute(conn)?;
diesel::delete(&notif).execute(&**conn)?;
}
Ok(())
+88 -107
View File
@@ -1,13 +1,13 @@
use activitypub::activity::*;
use serde_json;
use crate::{
comments::Comment,
db_conn::DbConn,
follows, likes,
posts::{Post, PostUpdate},
reshares::Reshare,
users::User,
Error, PlumeRocket,
Error, CONFIG,
};
use plume_common::activity_pub::inbox::Inbox;
@@ -46,20 +46,20 @@ impl_into_inbox_result! {
Reshare => Reshared
}
pub fn inbox(ctx: &PlumeRocket, act: serde_json::Value) -> Result<InboxResult, Error> {
Inbox::handle(ctx, act)
.with::<User, Announce, Post>()
.with::<User, Create, Comment>()
.with::<User, Create, Post>()
.with::<User, Delete, Comment>()
.with::<User, Delete, Post>()
.with::<User, Delete, User>()
.with::<User, Follow, User>()
.with::<User, Like, Post>()
.with::<User, Undo, Reshare>()
.with::<User, Undo, follows::Follow>()
.with::<User, Undo, likes::Like>()
.with::<User, Update, PostUpdate>()
pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result<InboxResult, Error> {
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()
}
@@ -67,13 +67,13 @@ pub fn inbox(ctx: &PlumeRocket, act: serde_json::Value) -> Result<InboxResult, E
pub(crate) mod tests {
use super::InboxResult;
use crate::blogs::tests::fill_database as blog_fill_db;
use crate::db_conn::DbConn;
use crate::safe_string::SafeString;
use crate::tests::rockets;
use crate::PlumeRocket;
use crate::tests::db;
use diesel::Connection;
pub fn fill_database(
rockets: &PlumeRocket,
conn: &DbConn,
) -> (
Vec<crate::posts::Post>,
Vec<crate::users::User>,
@@ -82,9 +82,9 @@ pub(crate) mod tests {
use crate::post_authors::*;
use crate::posts::*;
let (users, blogs) = blog_fill_db(&rockets.conn);
let (users, blogs) = blog_fill_db(&conn);
let post = Post::insert(
&rockets.conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "testing".to_owned(),
@@ -98,12 +98,11 @@ pub(crate) mod tests {
source: String::new(),
cover_id: None,
},
&rockets.searcher,
)
.unwrap();
PostAuthor::insert(
&rockets.conn,
&conn,
NewPostAuthor {
post_id: post.id,
author_id: users[0].id,
@@ -116,10 +115,9 @@ pub(crate) mod tests {
#[test]
fn announce_post() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&r);
let (posts, users, _) = fill_database(&conn);
let act = json!({
"id": "https://plu.me/announce/1",
"actor": users[0].ap_url,
@@ -127,7 +125,7 @@ pub(crate) mod tests {
"type": "Announce",
});
match super::inbox(&r, act).unwrap() {
match super::inbox(&conn, act).unwrap() {
super::InboxResult::Reshared(r) => {
assert_eq!(r.post_id, posts[0].id);
assert_eq!(r.user_id, users[0].id);
@@ -141,10 +139,9 @@ pub(crate) mod tests {
#[test]
fn create_comment() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&r);
let (posts, users, _) = fill_database(&conn);
let act = json!({
"id": "https://plu.me/comment/1/activity",
"actor": users[0].ap_url,
@@ -159,7 +156,7 @@ pub(crate) mod tests {
"type": "Create",
});
match super::inbox(&r, act).unwrap() {
match super::inbox(&conn, act).unwrap() {
super::InboxResult::Commented(c) => {
assert_eq!(c.author_id, users[0].id);
assert_eq!(c.post_id, posts[0].id);
@@ -175,10 +172,9 @@ pub(crate) mod tests {
#[test]
fn spoof_comment() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&r);
let (posts, users, _) = fill_database(&conn);
let act = json!({
"id": "https://plu.me/comment/1/activity",
"actor": users[0].ap_url,
@@ -194,7 +190,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&r, act.clone()),
super::inbox(&conn, act.clone()),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
@@ -205,10 +201,9 @@ pub(crate) mod tests {
#[test]
fn spoof_comment_by_object_with_id() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&r);
let (posts, users, _) = fill_database(&conn);
let act = json!({
"id": "https://plu.me/comment/1/activity",
"actor": users[0].ap_url,
@@ -226,7 +221,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&r, act.clone()),
super::inbox(&conn, act.clone()),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
@@ -236,10 +231,9 @@ pub(crate) mod tests {
}
#[test]
fn spoof_comment_by_object_without_id() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&r);
let (posts, users, _) = fill_database(&conn);
let act = json!({
"id": "https://plu.me/comment/1/activity",
"actor": users[0].ap_url,
@@ -255,7 +249,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&r, act.clone()),
super::inbox(&conn, act.clone()),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
@@ -266,10 +260,9 @@ pub(crate) mod tests {
#[test]
fn create_post() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (_, users, blogs) = fill_database(&r);
let (_, users, blogs) = fill_database(&conn);
let act = json!({
"id": "https://plu.me/comment/1/activity",
"actor": users[0].ap_url,
@@ -290,9 +283,9 @@ pub(crate) mod tests {
"type": "Create",
});
match super::inbox(&r, act).unwrap() {
match super::inbox(&conn, act).unwrap() {
super::InboxResult::Post(p) => {
assert!(p.is_author(conn, users[0].id).unwrap());
assert!(p.is_author(&conn, users[0].id).unwrap());
assert_eq!(p.source, "Hello.".to_owned());
assert_eq!(p.blog_id, blogs[0].id);
assert_eq!(p.content, SafeString::new("Hello."));
@@ -307,10 +300,9 @@ pub(crate) mod tests {
#[test]
fn spoof_post() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (_, users, blogs) = fill_database(&r);
let (_, users, blogs) = fill_database(&conn);
let act = json!({
"id": "https://plu.me/comment/1/activity",
"actor": users[0].ap_url,
@@ -332,7 +324,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&r, act.clone()),
super::inbox(&conn, act.clone()),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
@@ -343,10 +335,9 @@ pub(crate) mod tests {
#[test]
fn spoof_post_by_object_with_id() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (_, users, blogs) = fill_database(&r);
let (_, users, blogs) = fill_database(&conn);
let act = json!({
"id": "https://plu.me/comment/1/activity",
"actor": users[0].ap_url,
@@ -371,7 +362,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&r, act.clone()),
super::inbox(&conn, act.clone()),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
@@ -382,10 +373,9 @@ pub(crate) mod tests {
#[test]
fn spoof_post_by_object_without_id() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (_, users, blogs) = fill_database(&r);
let (_, users, blogs) = fill_database(&conn);
let act = json!({
"id": "https://plu.me/comment/1/activity",
"actor": users[0].ap_url,
@@ -407,7 +397,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&r, act.clone()),
super::inbox(&conn, act.clone()),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
@@ -420,12 +410,11 @@ pub(crate) mod tests {
fn delete_comment() {
use crate::comments::*;
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&r);
let (posts, users, _) = fill_database(&conn);
Comment::insert(
conn,
&conn,
NewComment {
content: SafeString::new("My comment"),
in_response_to_id: None,
@@ -445,7 +434,7 @@ pub(crate) mod tests {
"object": "https://plu.me/comment/1",
"type": "Delete",
});
assert!(super::inbox(&r, fail_act).is_err());
assert!(super::inbox(&conn, fail_act).is_err());
let ok_act = json!({
"id": "https://plu.me/comment/1/delete",
@@ -453,17 +442,16 @@ pub(crate) mod tests {
"object": "https://plu.me/comment/1",
"type": "Delete",
});
assert!(super::inbox(&r, ok_act).is_ok());
assert!(super::inbox(&conn, ok_act).is_ok());
Ok(())
})
}
#[test]
fn delete_post() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&r);
let (posts, users, _) = fill_database(&conn);
let fail_act = json!({
"id": "https://plu.me/comment/1/delete",
@@ -471,7 +459,7 @@ pub(crate) mod tests {
"object": posts[0].ap_url,
"type": "Delete",
});
assert!(super::inbox(&r, fail_act).is_err());
assert!(super::inbox(&conn, fail_act).is_err());
let ok_act = json!({
"id": "https://plu.me/comment/1/delete",
@@ -479,17 +467,16 @@ pub(crate) mod tests {
"object": posts[0].ap_url,
"type": "Delete",
});
assert!(super::inbox(&r, ok_act).is_ok());
assert!(super::inbox(&conn, ok_act).is_ok());
Ok(())
});
}
#[test]
fn delete_user() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (_, users, _) = fill_database(&r);
let (_, users, _) = fill_database(&conn);
let fail_act = json!({
"id": "https://plu.me/@/Admin#delete",
@@ -497,7 +484,7 @@ pub(crate) mod tests {
"object": users[0].ap_url,
"type": "Delete",
});
assert!(super::inbox(&r, fail_act).is_err());
assert!(super::inbox(&conn, fail_act).is_err());
let ok_act = json!({
"id": "https://plu.me/@/Admin#delete",
@@ -505,8 +492,8 @@ pub(crate) mod tests {
"object": users[0].ap_url,
"type": "Delete",
});
assert!(super::inbox(&r, ok_act).is_ok());
assert!(crate::users::User::get(conn, users[0].id).is_err());
assert!(super::inbox(&conn, ok_act).is_ok());
assert!(crate::users::User::get(&conn, users[0].id).is_err());
Ok(())
});
@@ -514,10 +501,9 @@ pub(crate) mod tests {
#[test]
fn follow() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (_, users, _) = fill_database(&r);
let (_, users, _) = fill_database(&conn);
let act = json!({
"id": "https://plu.me/follow/1",
@@ -525,7 +511,7 @@ pub(crate) mod tests {
"object": users[1].ap_url,
"type": "Follow",
});
match super::inbox(&r, act).unwrap() {
match super::inbox(&conn, act).unwrap() {
InboxResult::Followed(f) => {
assert_eq!(f.follower_id, users[0].id);
assert_eq!(f.following_id, users[1].id);
@@ -539,10 +525,9 @@ pub(crate) mod tests {
#[test]
fn like() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&r);
let (posts, users, _) = fill_database(&conn);
let act = json!({
"id": "https://plu.me/like/1",
@@ -550,7 +535,7 @@ pub(crate) mod tests {
"object": posts[0].ap_url,
"type": "Like",
});
match super::inbox(&r, act).unwrap() {
match super::inbox(&conn, act).unwrap() {
InboxResult::Liked(l) => {
assert_eq!(l.user_id, users[1].id);
assert_eq!(l.post_id, posts[0].id);
@@ -566,13 +551,12 @@ pub(crate) mod tests {
fn undo_reshare() {
use crate::reshares::*;
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&r);
let (posts, users, _) = fill_database(&conn);
let announce = Reshare::insert(
conn,
&conn,
NewReshare {
post_id: posts[0].id,
user_id: users[1].id,
@@ -587,7 +571,7 @@ pub(crate) mod tests {
"object": announce.ap_url,
"type": "Undo",
});
assert!(super::inbox(&r, fail_act).is_err());
assert!(super::inbox(&conn, fail_act).is_err());
let ok_act = json!({
"id": "https://plu.me/undo/1",
@@ -595,7 +579,7 @@ pub(crate) mod tests {
"object": announce.ap_url,
"type": "Undo",
});
assert!(super::inbox(&r, ok_act).is_ok());
assert!(super::inbox(&conn, ok_act).is_ok());
Ok(())
});
}
@@ -604,13 +588,12 @@ pub(crate) mod tests {
fn undo_follow() {
use crate::follows::*;
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (_, users, _) = fill_database(&r);
let (_, users, _) = fill_database(&conn);
let follow = Follow::insert(
conn,
&conn,
NewFollow {
follower_id: users[0].id,
following_id: users[1].id,
@@ -625,7 +608,7 @@ pub(crate) mod tests {
"object": follow.ap_url,
"type": "Undo",
});
assert!(super::inbox(&r, fail_act).is_err());
assert!(super::inbox(&conn, fail_act).is_err());
let ok_act = json!({
"id": "https://plu.me/undo/1",
@@ -633,7 +616,7 @@ pub(crate) mod tests {
"object": follow.ap_url,
"type": "Undo",
});
assert!(super::inbox(&r, ok_act).is_ok());
assert!(super::inbox(&conn, ok_act).is_ok());
Ok(())
});
}
@@ -642,13 +625,12 @@ pub(crate) mod tests {
fn undo_like() {
use crate::likes::*;
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&r);
let (posts, users, _) = fill_database(&conn);
let like = Like::insert(
conn,
&conn,
NewLike {
post_id: posts[0].id,
user_id: users[1].id,
@@ -663,7 +645,7 @@ pub(crate) mod tests {
"object": like.ap_url,
"type": "Undo",
});
assert!(super::inbox(&r, fail_act).is_err());
assert!(super::inbox(&conn, fail_act).is_err());
let ok_act = json!({
"id": "https://plu.me/undo/1",
@@ -671,17 +653,16 @@ pub(crate) mod tests {
"object": like.ap_url,
"type": "Undo",
});
assert!(super::inbox(&r, ok_act).is_ok());
assert!(super::inbox(&conn, ok_act).is_ok());
Ok(())
});
}
#[test]
fn update_post() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&r);
let (posts, users, _) = fill_database(&conn);
let act = json!({
"id": "https://plu.me/update/1",
@@ -700,7 +681,7 @@ pub(crate) mod tests {
"type": "Update",
});
super::inbox(&r, act).unwrap();
super::inbox(&conn, act).unwrap();
Ok(())
});
}
+48 -4
View File
@@ -3,11 +3,12 @@ use crate::{
medias::Media,
safe_string::SafeString,
schema::{instances, users},
users::{Role, User},
users::{NewUser, Role, User},
Connection, Error, Result,
};
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl};
use once_cell::sync::OnceCell;
use plume_common::utils::md_to_html;
use std::sync::RwLock;
@@ -45,6 +46,9 @@ lazy_static! {
static ref LOCAL_INSTANCE: RwLock<Option<Instance>> = RwLock::new(None);
}
const LOCAL_INSTANCE_USERNAME: &str = "__instance__";
static LOCAL_INSTANCE_USER: OnceCell<User> = OnceCell::new();
impl Instance {
pub fn set_local(self) {
LOCAL_INSTANCE.write().unwrap().replace(self);
@@ -76,6 +80,42 @@ impl Instance {
.map_err(Error::from)
}
pub fn create_local_instance_user(conn: &Connection) -> Result<User> {
let instance = Instance::get_local()?;
let email = format!("{}@{}", LOCAL_INSTANCE_USERNAME, &instance.public_domain);
NewUser::new_local(
conn,
LOCAL_INSTANCE_USERNAME.into(),
instance.public_domain,
Role::Instance,
"Local instance",
email,
None,
)
}
pub fn get_local_instance_user() -> Option<&'static User> {
LOCAL_INSTANCE_USER.get()
}
pub fn get_local_instance_user_uncached(conn: &Connection) -> Result<User> {
users::table
.filter(users::role.eq(3))
.first(conn)
.or_else(|err| match err {
NotFound => Self::create_local_instance_user(conn),
_ => Err(Error::Db(err)),
})
}
pub fn cache_local_instance_user(conn: &Connection) {
let _ = LOCAL_INSTANCE_USER.get_or_init(|| {
Self::get_local_instance_user_uncached(conn)
.or_else(|_| Self::create_local_instance_user(conn))
.expect("Failed to cache local instance user")
});
}
pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<Instance>> {
instances::table
.order(instances::public_domain.asc())
@@ -247,6 +287,7 @@ pub(crate) mod tests {
use diesel::Connection;
pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> {
diesel::delete(instances::table).execute(conn).unwrap();
let res = vec![
NewInstance {
default_license: "WTFPL".to_string(),
@@ -303,6 +344,7 @@ pub(crate) mod tests {
})
.collect();
Instance::cache_local(conn);
Instance::cache_local_instance_user(conn);
res
}
@@ -488,12 +530,14 @@ pub(crate) mod tests {
);
assert_eq!(
inst.long_description_html,
SafeString::new("<p><a href=\"/with_link\">long_description</a></p>\n")
SafeString::new(
"<p dir=\"auto\"><a href=\"/with_link\">long_description</a></p>\n"
)
);
assert_eq!(inst.short_description.get(), "[short](#link)");
assert_eq!(
inst.short_description_html,
SafeString::new("<p><a href=\"#link\">short</a></p>\n")
SafeString::new("<p dir=\"auto\"><a href=\"#link\">short</a></p>\n")
);
assert_eq!(inst.default_license, "CC-BY-SAO".to_owned());
Ok(())
+64 -21
View File
@@ -1,4 +1,3 @@
#![feature(try_trait)]
#![feature(never_type)]
#![feature(proc_macro_hygiene)]
#![feature(box_patterns)]
@@ -11,14 +10,19 @@ extern crate lazy_static;
extern crate plume_macro;
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;
#[macro_use]
extern crate tantivy;
use plume_common::activity_pub::inbox::InboxError;
pub use lettre;
pub use lettre::smtp;
use once_cell::sync::Lazy;
use plume_common::activity_pub::{inbox::InboxError, request, sign};
use posts::PostEvent;
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
use users::UserEvent;
#[cfg(not(any(feature = "sqlite", feature = "postgres")))]
compile_error!("Either feature \"sqlite\" or \"postgres\" must be enabled for this crate.");
@@ -31,6 +35,19 @@ pub type Connection = diesel::SqliteConnection;
#[cfg(all(not(feature = "sqlite"), feature = "postgres"))]
pub type Connection = diesel::PgConnection;
pub(crate) static ACTOR_SYS: Lazy<ActorSystem> = Lazy::new(|| {
SystemBuilder::new()
.name("plume")
.create()
.expect("Failed to create actor system")
});
pub(crate) static USER_CHAN: Lazy<ChannelRef<UserEvent>> =
Lazy::new(|| channel("user_events", &*ACTOR_SYS).expect("Failed to create user channel"));
pub(crate) static POST_CHAN: Lazy<ChannelRef<PostEvent>> =
Lazy::new(|| channel("post_events", &*ACTOR_SYS).expect("Failed to create post channel"));
/// All the possible errors that can be encoutered in this crate
#[derive(Debug)]
pub enum Error {
@@ -50,6 +67,7 @@ pub enum Error {
Url,
Webfinger,
Expired,
UserAlreadyExists,
}
impl From<bcrypt::BcryptError> for Error {
@@ -64,15 +82,15 @@ impl From<openssl::error::ErrorStack> for Error {
}
}
impl From<diesel::result::Error> for Error {
fn from(err: diesel::result::Error) -> Self {
Error::Db(err)
impl From<sign::Error> for Error {
fn from(_: sign::Error) -> Self {
Error::Signature
}
}
impl From<std::option::NoneError> for Error {
fn from(_: std::option::NoneError) -> Self {
Error::NotFound
impl From<diesel::result::Error> for Error {
fn from(err: diesel::result::Error) -> Self {
Error::Db(err)
}
}
@@ -142,6 +160,12 @@ impl From<InboxError<Error>> for Error {
}
}
impl From<request::Error> for Error {
fn from(_err: request::Error) -> Error {
Error::Request
}
}
pub type Result<T> = std::result::Result<T, Error>;
/// Adds a function to a model, that returns the first
@@ -279,15 +303,40 @@ pub fn ap_url(url: &str) -> String {
format!("https://{}", url)
}
pub trait SmtpNewWithAddr {
fn new_with_addr(
addr: (&str, u16),
) -> std::result::Result<smtp::SmtpClient, smtp::error::Error>;
}
impl SmtpNewWithAddr for smtp::SmtpClient {
// Stolen from lettre::smtp::SmtpClient::new_simple()
fn new_with_addr(addr: (&str, u16)) -> std::result::Result<Self, smtp::error::Error> {
use native_tls::TlsConnector;
use smtp::{
client::net::{ClientTlsParameters, DEFAULT_TLS_PROTOCOLS},
ClientSecurity, SmtpClient,
};
let (domain, port) = addr;
let mut tls_builder = TlsConnector::builder();
tls_builder.min_protocol_version(Some(DEFAULT_TLS_PROTOCOLS[0]));
let tls_parameters =
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
SmtpClient::new((domain, port), ClientSecurity::Wrapper(tls_parameters))
}
}
#[cfg(test)]
#[macro_use]
mod tests {
use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, search, Connection as Conn, CONFIG};
use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, Connection as Conn, CONFIG};
use diesel::r2d2::ConnectionManager;
use plume_common::utils::random_hex;
use scheduled_thread_pool::ScheduledThreadPool;
use std::env::temp_dir;
use std::sync::Arc;
#[macro_export]
macro_rules! part_eq {
@@ -317,15 +366,6 @@ mod tests {
pool
};
}
pub fn rockets() -> super::PlumeRocket {
super::PlumeRocket {
conn: db_conn::DbConn((*DB_POOL).get().unwrap()),
searcher: Arc::new(search::tests::get_searcher(&CONFIG.search_tokenizers)),
worker: Arc::new(ScheduledThreadPool::new(2)),
user: None,
}
}
}
pub mod admin;
@@ -337,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;
@@ -351,11 +392,13 @@ pub mod password_reset_requests;
pub mod plume_rocket;
pub mod post_authors;
pub mod posts;
pub mod remote_fetch_actor;
pub mod reshares;
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;
+38 -24
View File
@@ -1,12 +1,13 @@
use crate::{
notifications::*, posts::Post, schema::likes, timeline::*, users::User, Connection, Error,
PlumeRocket, Result,
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*,
users::User, Connection, Error, Result, CONFIG,
};
use activitypub::activity;
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY,
};
@@ -83,64 +84,77 @@ impl Like {
}
}
impl AsObject<User, activity::Like, &PlumeRocket> for Post {
impl AsObject<User, activity::Like, &DbConn> for Post {
type Error = Error;
type Output = Like;
fn activity(self, c: &PlumeRocket, actor: User, id: &str) -> Result<Like> {
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Like> {
let res = Like::insert(
&c.conn,
conn,
NewLike {
post_id: self.id,
user_id: actor.id,
ap_url: id.to_string(),
},
)?;
res.notify(&c.conn)?;
res.notify(conn)?;
Timeline::add_to_all_timelines(c, &self, Kind::Like(&actor))?;
Timeline::add_to_all_timelines(conn, &self, Kind::Like(&actor))?;
Ok(res)
}
}
impl FromId<PlumeRocket> for Like {
impl FromId<DbConn> for Like {
type Error = Error;
type Object = activity::Like;
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Like::find_by_ap_url(&c.conn, id)
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Like::find_by_ap_url(conn, id)
}
fn from_activity(c: &PlumeRocket, act: activity::Like) -> Result<Self> {
fn from_activity(conn: &DbConn, act: activity::Like) -> Result<Self> {
let res = Like::insert(
&c.conn,
conn,
NewLike {
post_id: Post::from_id(c, &act.like_props.object_link::<Id>()?, None)
.map_err(|(_, e)| e)?
.id,
user_id: User::from_id(c, &act.like_props.actor_link::<Id>()?, None)
.map_err(|(_, e)| e)?
.id,
post_id: Post::from_id(
conn,
&act.like_props.object_link::<Id>()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
user_id: User::from_id(
conn,
&act.like_props.actor_link::<Id>()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
ap_url: act.object_props.id_string()?,
},
)?;
res.notify(&c.conn)?;
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, &PlumeRocket> for Like {
impl AsObject<User, activity::Undo, &DbConn> for Like {
type Error = Error;
type Output = ();
fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
let conn = &*c.conn;
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
if actor.id == self.user_id {
diesel::delete(&self).execute(conn)?;
diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
diesel::delete(&notif).execute(conn)?;
diesel::delete(&notif).execute(&**conn)?;
}
Ok(())
} else {
+12 -10
View File
@@ -30,9 +30,9 @@ impl TryFrom<i32> for ListType {
}
}
impl Into<i32> for ListType {
fn into(self) -> i32 {
match self {
impl From<ListType> for i32 {
fn from(list_type: ListType) -> Self {
match list_type {
ListType::User => 0,
ListType::Blog => 1,
ListType::Word => 2,
@@ -143,6 +143,7 @@ macro_rules! func {
}
}
#[allow(dead_code)]
#[derive(Clone, Queryable, Identifiable)]
struct ListElem {
pub id: i32,
@@ -246,22 +247,22 @@ impl List {
private::ListElem::prefix_in_list(conn, self, word)
}
/// Insert new users in a list
// Insert new users in a list
func! {add: add_users, User}
/// Insert new blogs in a list
// Insert new blogs in a list
func! {add: add_blogs, Blog}
/// Insert new words in a list
// Insert new words in a list
func! {add: add_words, Word}
/// Insert new prefixes in a list
// Insert new prefixes in a list
func! {add: add_prefixes, Prefix}
/// Get all users in the list
// Get all users in the list
func! {list: list_users, User, users}
/// Get all blogs in the list
// Get all blogs in the list
func! {list: list_blogs, Blog, blogs}
/// Get all words in the list
@@ -285,7 +286,8 @@ impl List {
.select(list_elems::word)
.load::<Option<String>>(conn)
.map_err(Error::from)
.map(|r| r.into_iter().filter_map(|o| o).collect::<Vec<String>>())
// .map(|r| r.into_iter().filter_map(|o| o).collect::<Vec<String>>())
.map(|r| r.into_iter().flatten().collect::<Vec<String>>())
}
pub fn clear(&self, conn: &Connection) -> Result<()> {
+133 -55
View File
@@ -1,19 +1,24 @@
use crate::{
ap_url, instance::Instance, safe_string::SafeString, schema::medias, users::User, Connection,
Error, PlumeRocket, Result,
ap_url, db_conn::DbConn, instance::Instance, safe_string::SafeString, schema::medias,
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 reqwest;
use std::{fs, path::Path};
use std::{
fs::{self, DirBuilder},
path::{self, Path, PathBuf},
};
use tracing::warn;
use url::Url;
#[derive(Clone, Identifiable, Queryable)]
const REMOTE_MEDIA_DIRECTORY: &str = "remote";
#[derive(Clone, Identifiable, Queryable, AsChangeset)]
pub struct Media {
pub id: i32,
pub file_path: String,
@@ -59,6 +64,7 @@ impl MediaCategory {
impl Media {
insert!(medias, NewMedia);
get!(medias);
find_by!(medias, find_by_file_path, file_path as &str);
pub fn for_user(conn: &Connection, owner: i32) -> Result<Vec<Media>> {
medias::table
@@ -97,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()
{
@@ -149,12 +155,15 @@ impl Media {
if self.is_remote {
Ok(self.remote_url.clone().unwrap_or_default())
} else {
let p = Path::new(&self.file_path);
let filename: String = p.file_name().unwrap().to_str().unwrap().to_owned();
let file_path = self.file_path.replace(path::MAIN_SEPARATOR, "/").replacen(
&CONFIG.media_directory,
"static/media",
1,
); // "static/media" from plume::routs::plume_media_files()
Ok(ap_url(&format!(
"{}/static/media/{}",
"{}/{}",
Instance::get_local()?.public_domain,
&filename
&file_path
)))
}
}
@@ -197,50 +206,92 @@ impl Media {
}
// TODO: merge with save_remote?
pub fn from_activity(c: &PlumeRocket, image: &Image) -> Result<Media> {
let conn = &*c.conn;
let remote_url = image.object_props.url_string().ok()?;
let ext = remote_url
.rsplit('.')
.next()
.map(ToOwned::to_owned)
.unwrap_or_else(|| String::from("png"));
let path = Path::new(&super::CONFIG.media_directory).join(format!(
"{}.{}",
GUID::rand().to_string(),
ext
));
pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> {
let remote_url = image
.object_props
.url_string()
.or(Err(Error::MissingApProperty))?;
let path = determine_mirror_file_path(&remote_url);
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()?;
reqwest::get(remote_url.as_str())
.ok()?
.copy_to(&mut dest)
.ok()?;
let mut dest = fs::File::create(path.clone())?;
// TODO: conditional GET
request::get(
remote_url.as_str(),
User::get_sender(),
CONFIG.proxy().cloned(),
)?
.copy_to(&mut dest)?;
Media::insert(
conn,
NewMedia {
file_path: path.to_str()?.to_string(),
alt_text: image.object_props.content_string().ok()?,
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(
c,
image
.object_props
.attributed_to_link_vec::<Id>()
.ok()?
.into_iter()
.next()?
.as_ref(),
None,
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()
.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 {
media.alt_text = alt_text;
updated = true;
}
if media.is_remote {
media.is_remote = false;
updated = true;
}
if media.remote_url.is_some() {
media.remote_url = None;
updated = true;
}
if media.sensitive != sensitive {
media.sensitive = sensitive;
updated = true;
}
if media.content_warning != content_warning {
media.content_warning = content_warning;
updated = true;
}
if updated {
diesel::update(&media).set(&media).execute(&**conn)?;
}
Ok(media)
})
.or_else(|_| {
Media::insert(
conn,
NewMedia {
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,
image
.object_props
.attributed_to_link_vec::<Id>()
.or(Err(Error::NotFound))?
.into_iter()
.next()
.ok_or(Error::NotFound)?
.as_ref(),
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
},
)
.map_err(|(_, e)| e)?
.id,
},
)
})
}
pub fn get_media_processor<'a>(conn: &'a Connection, user: Vec<&User>) -> MediaProcessor<'a> {
@@ -257,6 +308,33 @@ impl Media {
}
}
fn determine_mirror_file_path(url: &str) -> PathBuf {
let mut file_path = Path::new(&super::CONFIG.media_directory).join(REMOTE_MEDIA_DIRECTORY);
Url::parse(url)
.map(|url| {
if !url.has_host() {
return;
}
file_path.push(url.host_str().unwrap());
for segment in url.path_segments().expect("FIXME") {
file_path.push(segment);
}
// TODO: handle query
// HINT: Use characters which must be percent-encoded in path as separator between path and query
// HINT: handle extension
})
.unwrap_or_else(|err| {
warn!("Failed to parse url: {} {}", &url, err);
let ext = url
.rsplit('.')
.next()
.map(ToOwned::to_owned)
.unwrap_or_else(|| String::from("png"));
file_path.push(format!("{}.{}", GUID::rand(), ext));
});
file_path
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
+10 -6
View File
@@ -1,6 +1,6 @@
use crate::{
comments::Comment, notifications::*, posts::Post, schema::mentions, users::User, Connection,
Error, PlumeRocket, Result,
comments::Comment, db_conn::DbConn, notifications::*, posts::Post, schema::mentions,
users::User, Connection, Error, Result,
};
use activitypub::link;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
@@ -47,13 +47,17 @@ 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)),
}
}
pub fn build_activity(c: &PlumeRocket, ment: &str) -> Result<link::Mention> {
let user = User::find_by_fqn(c, ment)?;
pub fn build_activity(conn: &DbConn, ment: &str) -> Result<link::Mention> {
let user = User::find_by_fqn(conn, ment)?;
let mut mention = link::Mention::default();
mention.link_props.set_href_string(user.ap_url)?;
mention.link_props.set_name_string(format!("@{}", ment))?;
@@ -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 {
+5 -3
View File
@@ -2,6 +2,7 @@ use crate::{Connection, Error, Result};
use diesel::connection::{Connection as Conn, SimpleConnection};
use migrations_internals::{setup_database, MigrationConnection};
use std::path::Path;
use tracing::info;
#[allow(dead_code)] //variants might not be constructed if not required by current migrations
enum Action {
@@ -26,7 +27,7 @@ struct ComplexMigration {
impl ComplexMigration {
fn run(&self, conn: &Connection, path: &Path) -> Result<()> {
println!("Running migration {}", self.name);
info!("Running migration {}", self.name);
for step in self.up {
step.run(conn, path)?
}
@@ -34,7 +35,7 @@ impl ComplexMigration {
}
fn revert(&self, conn: &Connection, path: &Path) -> Result<()> {
println!("Reverting migration {}", self.name);
info!("Reverting migration {}", self.name);
for step in self.down {
step.run(conn, path)?
}
@@ -104,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));
+2 -8
View File
@@ -2,7 +2,7 @@ pub use self::module::PlumeRocket;
#[cfg(not(test))]
mod module {
use crate::{db_conn::DbConn, search, users};
use crate::{search, users};
use rocket::{
request::{self, FlashMessage, FromRequest, Request},
Outcome, State,
@@ -12,7 +12,6 @@ mod module {
/// Common context needed by most routes and operations on models
pub struct PlumeRocket {
pub conn: DbConn,
pub intl: rocket_i18n::I18n,
pub user: Option<users::User>,
pub searcher: Arc<search::Searcher>,
@@ -24,14 +23,12 @@ mod module {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> {
let conn = request.guard::<DbConn>()?;
let intl = request.guard::<rocket_i18n::I18n>()?;
let user = request.guard::<users::User>().succeeded();
let worker = request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>()?;
let searcher = request.guard::<'_, State<'_, Arc<search::Searcher>>>()?;
let flash_msg = request.guard::<FlashMessage<'_, '_>>().succeeded();
Outcome::Success(PlumeRocket {
conn,
intl,
user,
flash_msg: flash_msg.map(|f| (f.name().into(), f.msg().into())),
@@ -44,7 +41,7 @@ mod module {
#[cfg(test)]
mod module {
use crate::{db_conn::DbConn, search, users};
use crate::{search, users};
use rocket::{
request::{self, FromRequest, Request},
Outcome, State,
@@ -54,7 +51,6 @@ mod module {
/// Common context needed by most routes and operations on models
pub struct PlumeRocket {
pub conn: DbConn,
pub user: Option<users::User>,
pub searcher: Arc<search::Searcher>,
pub worker: Arc<ScheduledThreadPool>,
@@ -64,12 +60,10 @@ mod module {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> {
let conn = request.guard::<DbConn>()?;
let user = request.guard::<users::User>().succeeded();
let worker = request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>()?;
let searcher = request.guard::<'_, State<'_, Arc<search::Searcher>>>()?;
Outcome::Success(PlumeRocket {
conn,
user,
worker: worker.clone(),
searcher: searcher.clone(),
+251 -115
View File
@@ -1,7 +1,7 @@
use crate::{
ap_url, blogs::Blog, instance::Instance, medias::Media, mentions::Mention, post_authors::*,
safe_string::SafeString, schema::posts, search::Searcher, tags::*, timeline::*, users::User,
Connection, Error, PlumeRocket, Result, CONFIG,
ap_url, blogs::Blog, db_conn::DbConn, instance::Instance, medias::Media, mentions::Mention,
post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User,
Connection, Error, PostEvent::*, Result, CONFIG, POST_CHAN,
};
use activitypub::{
activity::{Create, Delete, Update},
@@ -10,21 +10,25 @@ use activitypub::{
CustomObject,
};
use chrono::{NaiveDateTime, TimeZone, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use heck::KebabCase;
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl};
use once_cell::sync::Lazy;
use plume_common::{
activity_pub::{
inbox::{AsObject, FromId},
inbox::{AsActor, AsObject, FromId},
sign::Signer,
Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY,
},
utils::md_to_html,
utils::{iri_percent_encode_seg, md_to_html},
};
use serde_json;
use std::collections::HashSet;
use riker::actors::{Publish, Tell};
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
pub type LicensedArticle = CustomObject<Licensed, Article>;
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
static BLOG_FQN_CACHE: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| Mutex::new(HashMap::new()));
#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)]
#[changeset_options(treat_none_as_null = "true")]
pub struct Post {
pub id: i32,
@@ -63,38 +67,42 @@ impl Post {
find_by!(posts, find_by_ap_url, ap_url as &str);
last!(posts);
pub fn insert(conn: &Connection, new: NewPost, searcher: &Searcher) -> Result<Self> {
pub fn insert(conn: &Connection, mut new: NewPost) -> Result<Self> {
if new.ap_url.is_empty() {
let blog = Blog::get(conn, new.blog_id)?;
new.ap_url = Self::ap_url(blog, &new.slug);
}
diesel::insert_into(posts::table)
.values(new)
.execute(conn)?;
let mut post = Self::last(conn)?;
if post.ap_url.is_empty() {
post.ap_url = ap_url(&format!(
"{}/~/{}/{}/",
CONFIG.base_url,
post.get_blog(conn)?.fqn,
post.slug
));
let _: Post = post.save_changes(conn)?;
let post = Self::last(conn)?;
if post.published {
post.publish_published();
}
searcher.add_document(conn, &post)?;
Ok(post)
}
pub fn update(&self, conn: &Connection, searcher: &Searcher) -> Result<Self> {
pub fn update(&self, conn: &Connection) -> Result<Self> {
diesel::update(self).set(self).execute(conn)?;
let post = Self::get(conn, self.id)?;
searcher.update_document(conn, &post)?;
// TODO: Call publish_published() when newly published
if post.published {
let blog = post.get_blog(conn);
if blog.is_ok() && blog.unwrap().is_local() {
self.publish_updated();
}
}
Ok(post)
}
pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
for m in Mention::list_for_post(&conn, self.id)? {
pub fn delete(&self, conn: &Connection) -> Result<()> {
for m in Mention::list_for_post(conn, self.id)? {
m.delete(conn)?;
}
diesel::delete(self).execute(conn)?;
searcher.delete_document(self);
self.publish_deleted();
Ok(())
}
@@ -124,8 +132,7 @@ impl Post {
.filter(posts::published.eq(true))
.count()
.load(conn)?
.iter()
.next()
.get(0)
.cloned()
.ok_or(Error::NotFound)
}
@@ -242,6 +249,20 @@ impl Post {
.map_err(Error::from)
}
pub fn ap_url(blog: Blog, slug: &str) -> String {
ap_url(&format!(
"{}/~/{}/{}/",
CONFIG.base_url,
blog.fqn,
iri_percent_encode_seg(slug)
))
}
// It's better to calc slug in insert and update
pub fn slug(title: &str) -> &str {
title
}
pub fn get_authors(&self, conn: &Connection) -> Result<Vec<User>> {
use crate::schema::post_authors;
use crate::schema::users;
@@ -269,6 +290,24 @@ impl Post {
.map_err(Error::from)
}
/// This method exists for use in templates to reduce database access.
/// This should not be used for other purpose.
///
/// This caches query result. The best way to cache query result is holding it in `Post`s field
/// but Diesel doesn't allow it currently.
/// If sometime Diesel allow it, this method should be removed.
pub fn get_blog_fqn(&self, conn: &Connection) -> String {
if let Some(blog_fqn) = BLOG_FQN_CACHE.lock().unwrap().get(&self.blog_id) {
return blog_fqn.to_string();
}
let blog_fqn = self.get_blog(conn).unwrap().fqn;
BLOG_FQN_CACHE
.lock()
.unwrap()
.insert(self.blog_id, blog_fqn.clone());
blog_fqn
}
pub fn count_likes(&self, conn: &Connection) -> Result<i64> {
use crate::schema::likes;
likes::table
@@ -288,17 +327,16 @@ impl Post {
}
pub fn get_receivers_urls(&self, conn: &Connection) -> Result<Vec<String>> {
let followers = self
Ok(self
.get_authors(conn)?
.into_iter()
.filter_map(|a| a.get_followers(conn).ok())
.collect::<Vec<Vec<User>>>();
Ok(followers.into_iter().fold(vec![], |mut acc, f| {
for x in f {
acc.push(x.ap_url);
}
acc
}))
.fold(vec![], |mut acc, f| {
for x in f {
acc.push(x.ap_url);
}
acc
}))
}
pub fn to_activity(&self, conn: &Connection) -> Result<LicensedArticle> {
@@ -417,23 +455,17 @@ impl Post {
m,
)
})
.filter_map(|(id, m)| {
if let Some(id) = id {
Some((m, id))
} else {
None
}
})
.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)?;
}
}
@@ -445,7 +477,7 @@ impl Post {
.iter()
.filter(|m| !new_mentions.contains(&m.mentioned_id))
{
m.delete(&conn)?;
m.delete(conn)?;
}
Ok(())
}
@@ -548,19 +580,48 @@ impl Post {
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?;
Ok(act)
}
fn publish_published(&self) {
POST_CHAN.tell(
Publish {
msg: PostPublished(Arc::new(self.clone())),
topic: "post.published".into(),
},
None,
)
}
fn publish_updated(&self) {
POST_CHAN.tell(
Publish {
msg: PostUpdated(Arc::new(self.clone())),
topic: "post.updated".into(),
},
None,
)
}
fn publish_deleted(&self) {
POST_CHAN.tell(
Publish {
msg: PostDeleted(Arc::new(self.clone())),
topic: "post.deleted".into(),
},
None,
)
}
}
impl FromId<PlumeRocket> for Post {
impl FromId<DbConn> for Post {
type Error = Error;
type Object = LicensedArticle;
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Self::find_by_ap_url(&c.conn, id)
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id)
}
fn from_activity(c: &PlumeRocket, article: LicensedArticle) -> Result<Self> {
let conn = &*c.conn;
let searcher = &c.searcher;
fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result<Self> {
let conn = conn;
let license = article.custom_props.license_string().unwrap_or_default();
let article = article.object;
@@ -570,12 +631,15 @@ impl FromId<PlumeRocket> for Post {
.into_iter()
.fold((None, vec![]), |(blog, mut authors), link| {
let url = link;
match User::from_id(&c, &url, None) {
match User::from_id(conn, &url, None, CONFIG.proxy()) {
Ok(u) => {
authors.push(u);
(blog, authors)
}
Err(_) => (blog.or_else(|| Blog::from_id(&c, &url, None).ok()), authors),
Err(_) => (
blog.or_else(|| Blog::from_id(conn, &url, None, CONFIG.proxy()).ok()),
authors,
),
}
});
@@ -583,40 +647,88 @@ impl FromId<PlumeRocket> for Post {
.object_props
.icon_object::<Image>()
.ok()
.and_then(|img| Media::from_activity(&c, &img).ok().map(|m| m.id));
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id));
let title = article.object_props.name_string()?;
let post = Post::insert(
conn,
NewPost {
blog_id: blog?.id,
slug: title.to_kebab_case(),
title,
content: SafeString::new(&article.object_props.content_string()?),
published: true,
license,
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url: article
.object_props
.url_string()
.or_else(|_| article.object_props.id_string())?,
creation_date: Some(article.object_props.published_utctime()?.naive_utc()),
subtitle: article.object_props.summary_string()?,
source: article.ap_object_props.source_object::<Source>()?.content,
cover_id: cover,
},
searcher,
)?;
let ap_url = article
.object_props
.url_string()
.or_else(|_| article.object_props.id_string())?;
let post = Post::from_db(conn, &ap_url)
.and_then(|mut post| {
let mut updated = false;
for author in authors {
PostAuthor::insert(
conn,
NewPostAuthor {
post_id: post.id,
author_id: author.id,
},
)?;
}
let slug = Self::slug(&title);
let content = SafeString::new(&article.object_props.content_string()?);
let subtitle = article.object_props.summary_string()?;
let source = article.ap_object_props.source_object::<Source>()?.content;
if post.slug != slug {
post.slug = slug.to_string();
updated = true;
}
if post.title != title {
post.title = title.clone();
updated = true;
}
if post.content != content {
post.content = content;
updated = true;
}
if post.license != license {
post.license = license.clone();
updated = true;
}
if post.subtitle != subtitle {
post.subtitle = subtitle;
updated = true;
}
if post.source != source {
post.source = source;
updated = true;
}
if post.cover_id != cover {
post.cover_id = cover;
updated = true;
}
if updated {
post.update(conn)?;
}
Ok(post)
})
.or_else(|_| {
Post::insert(
conn,
NewPost {
blog_id: blog.ok_or(Error::NotFound)?.id,
slug: Self::slug(&title).to_string(),
title,
content: SafeString::new(&article.object_props.content_string()?),
published: true,
license,
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url,
creation_date: Some(article.object_props.published_utctime()?.naive_utc()),
subtitle: article.object_props.summary_string()?,
source: article.ap_object_props.source_object::<Source>()?.content,
cover_id: cover,
},
)
.and_then(|post| {
for author in authors {
PostAuthor::insert(
conn,
NewPostAuthor {
post_id: post.id,
author_id: author.id,
},
)?;
}
Ok(post)
})
})?;
// save mentions and tags
let mut hashtags = md_to_html(&post.source, None, false, None)
@@ -644,33 +756,37 @@ impl FromId<PlumeRocket> for Post {
}
}
Timeline::add_to_all_timelines(c, &post, Kind::Original)?;
Timeline::add_to_all_timelines(conn, &post, Kind::Original)?;
Ok(post)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl AsObject<User, Create, &PlumeRocket> for Post {
impl AsObject<User, Create, &DbConn> for Post {
type Error = Error;
type Output = Post;
fn activity(self, _c: &PlumeRocket, _actor: User, _id: &str) -> Result<Post> {
fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Post> {
// TODO: check that _actor is actually one of the author?
Ok(self)
}
}
impl AsObject<User, Delete, &PlumeRocket> for Post {
impl AsObject<User, Delete, &DbConn> for Post {
type Error = Error;
type Output = ();
fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
let can_delete = self
.get_authors(&c.conn)?
.get_authors(conn)?
.into_iter()
.any(|a| actor.id == a.id);
if can_delete {
self.delete(&c.conn, &c.searcher).map(|_| ())
self.delete(conn).map(|_| ())
} else {
Err(Error::Unauthorized)
}
@@ -688,16 +804,16 @@ pub struct PostUpdate {
pub tags: Option<serde_json::Value>,
}
impl FromId<PlumeRocket> for PostUpdate {
impl FromId<DbConn> for PostUpdate {
type Error = Error;
type Object = LicensedArticle;
fn from_db(_: &PlumeRocket, _: &str) -> Result<Self> {
fn from_db(_: &DbConn, _: &str) -> Result<Self> {
// Always fail because we always want to deserialize the AP object
Err(Error::NotFound)
}
fn from_activity(c: &PlumeRocket, updated: LicensedArticle) -> Result<Self> {
fn from_activity(conn: &DbConn, updated: LicensedArticle) -> Result<Self> {
Ok(PostUpdate {
ap_url: updated.object.object_props.id_string()?,
title: updated.object.object_props.name_string().ok(),
@@ -708,7 +824,7 @@ impl FromId<PlumeRocket> for PostUpdate {
.object_props
.icon_object::<Image>()
.ok()
.and_then(|img| Media::from_activity(&c, &img).ok().map(|m| m.id)),
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)),
source: updated
.object
.ap_object_props
@@ -719,16 +835,19 @@ impl FromId<PlumeRocket> 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, &PlumeRocket> for PostUpdate {
impl AsObject<User, Update, &DbConn> for PostUpdate {
type Error = Error;
type Output = ();
fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
let conn = &*c.conn;
let searcher = &c.searcher;
let mut post = Post::from_id(c, &self.ap_url, None).map_err(|(_, e)| e)?;
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
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
@@ -736,7 +855,7 @@ impl AsObject<User, Update, &PlumeRocket> for PostUpdate {
}
if let Some(title) = self.title {
post.slug = title.to_kebab_case();
post.slug = Post::slug(&title).to_string();
post.title = title;
}
@@ -789,7 +908,7 @@ impl AsObject<User, Update, &PlumeRocket> for PostUpdate {
post.update_hashtags(conn, hashtags)?;
}
post.update(conn, searcher)?;
post.update(conn)?;
Ok(())
}
}
@@ -800,24 +919,42 @@ impl IntoId for Post {
}
}
#[derive(Clone, Debug)]
pub enum PostEvent {
PostPublished(Arc<Post>),
PostUpdated(Arc<Post>),
PostDeleted(Arc<Post>),
}
impl From<PostEvent> for Arc<Post> {
fn from(event: PostEvent) -> Self {
use PostEvent::*;
match event {
PostPublished(post) => post,
PostUpdated(post) => post,
PostDeleted(post) => post,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inbox::{inbox, tests::fill_database, InboxResult};
use crate::safe_string::SafeString;
use crate::tests::rockets;
use crate::tests::db;
use diesel::Connection;
// creates a post, get it's Create activity, delete the post,
// "send" the Create to the inbox, and check it works
#[test]
fn self_federation() {
let r = rockets();
let conn = &*r.conn;
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (_, users, blogs) = fill_database(&r);
let (_, users, blogs) = fill_database(&conn);
let post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "yo".into(),
@@ -831,23 +968,22 @@ mod tests {
source: "Hello".into(),
cover_id: None,
},
&r.searcher,
)
.unwrap();
PostAuthor::insert(
conn,
&conn,
NewPostAuthor {
post_id: post.id,
author_id: users[0].id,
},
)
.unwrap();
let create = post.create_activity(conn).unwrap();
post.delete(conn, &r.searcher).unwrap();
let create = post.create_activity(&conn).unwrap();
post.delete(&conn).unwrap();
match inbox(&r, serde_json::to_value(create).unwrap()).unwrap() {
match inbox(&conn, serde_json::to_value(create).unwrap()).unwrap() {
InboxResult::Post(p) => {
assert!(p.is_author(conn, users[0].id).unwrap());
assert!(p.is_author(&conn, users[0].id).unwrap());
assert_eq!(p.source, "Hello".to_owned());
assert_eq!(p.blog_id, blogs[0].id);
assert_eq!(p.content, SafeString::new("Hello"));
+123
View File
@@ -0,0 +1,123 @@
use crate::{
db_conn::{DbConn, DbPool},
follows,
posts::{LicensedArticle, Post},
users::{User, UserEvent},
ACTOR_SYS, CONFIG, USER_CHAN,
};
use activitypub::activity::Create;
use plume_common::activity_pub::inbox::FromId;
use riker::actors::{Actor, ActorFactoryArgs, ActorRefFactory, Context, Sender, Subscribe, Tell};
use std::sync::Arc;
use tracing::{error, info, warn};
pub struct RemoteFetchActor {
conn: DbPool,
}
impl RemoteFetchActor {
pub fn init(conn: DbPool) {
let actor = ACTOR_SYS
.actor_of_args::<RemoteFetchActor, _>("remote-fetch", conn)
.expect("Failed to initialize remote fetch actor");
USER_CHAN.tell(
Subscribe {
actor: Box::new(actor),
topic: "*".into(),
},
None,
)
}
}
impl Actor for RemoteFetchActor {
type Msg = UserEvent;
fn recv(&mut self, _ctx: &Context<Self::Msg>, msg: Self::Msg, _sender: Sender) {
use UserEvent::*;
match msg {
RemoteUserFound(user) => match self.conn.get() {
Ok(conn) => {
let conn = DbConn(conn);
// Don't call these functions in parallel
// for the case database connections limit is too small
fetch_and_cache_articles(&user, &conn);
fetch_and_cache_followers(&user, &conn);
if user.needs_update() {
fetch_and_cache_user(&user, &conn);
}
}
_ => {
error!("Failed to get database connection");
}
},
}
}
}
impl ActorFactoryArgs<DbPool> for RemoteFetchActor {
fn create_args(conn: DbPool) -> Self {
Self { conn }
}
}
fn fetch_and_cache_articles(user: &Arc<User>, conn: &DbConn) {
let create_acts = user.fetch_outbox::<Create>();
match create_acts {
Ok(create_acts) => {
for create_act in create_acts {
match create_act.create_props.object_object::<LicensedArticle>() {
Ok(article) => {
Post::from_activity(conn, article)
.expect("Article from remote user couldn't be saved");
info!("Fetched article from remote user");
}
Err(e) => warn!("Error while fetching articles in background: {:?}", e),
}
}
}
Err(err) => {
error!("Failed to fetch outboxes: {:?}", err);
}
}
}
fn fetch_and_cache_followers(user: &Arc<User>, conn: &DbConn) {
let follower_ids = user.fetch_followers_ids();
match follower_ids {
Ok(user_ids) => {
for user_id in user_ids {
let follower = User::from_id(conn, &user_id, None, CONFIG.proxy());
match follower {
Ok(follower) => {
let inserted = follows::Follow::insert(
conn,
follows::NewFollow {
follower_id: follower.id,
following_id: user.id,
ap_url: String::new(),
},
);
if inserted.is_err() {
error!("Couldn't save follower for remote user: {:?}", user_id);
}
}
Err(err) => {
error!("Couldn't fetch follower: {:?}", err);
}
}
}
}
Err(err) => {
error!("Failed to fetch follower: {:?}", err);
}
}
}
fn fetch_and_cache_user(user: &Arc<User>, conn: &DbConn) {
if user.refetch(conn).is_err() {
error!("Couldn't update user info: {:?}", user);
}
}
+38 -24
View File
@@ -1,12 +1,13 @@
use crate::{
notifications::*, posts::Post, schema::reshares, timeline::*, users::User, Connection, Error,
PlumeRocket, Result,
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares,
timeline::*, users::User, Connection, Error, Result, CONFIG,
};
use activitypub::activity::{Announce, Undo};
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY,
};
@@ -107,12 +108,12 @@ impl Reshare {
}
}
impl AsObject<User, Announce, &PlumeRocket> for Post {
impl AsObject<User, Announce, &DbConn> for Post {
type Error = Error;
type Output = Reshare;
fn activity(self, c: &PlumeRocket, actor: User, id: &str) -> Result<Reshare> {
let conn = &*c.conn;
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Reshare> {
let conn = conn;
let reshare = Reshare::insert(
conn,
NewReshare {
@@ -123,49 +124,62 @@ impl AsObject<User, Announce, &PlumeRocket> for Post {
)?;
reshare.notify(conn)?;
Timeline::add_to_all_timelines(c, &self, Kind::Reshare(&actor))?;
Timeline::add_to_all_timelines(conn, &self, Kind::Reshare(&actor))?;
Ok(reshare)
}
}
impl FromId<PlumeRocket> for Reshare {
impl FromId<DbConn> for Reshare {
type Error = Error;
type Object = Announce;
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Reshare::find_by_ap_url(&c.conn, id)
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Reshare::find_by_ap_url(conn, id)
}
fn from_activity(c: &PlumeRocket, act: Announce) -> Result<Self> {
fn from_activity(conn: &DbConn, act: Announce) -> Result<Self> {
let res = Reshare::insert(
&c.conn,
conn,
NewReshare {
post_id: Post::from_id(c, &act.announce_props.object_link::<Id>()?, None)
.map_err(|(_, e)| e)?
.id,
user_id: User::from_id(c, &act.announce_props.actor_link::<Id>()?, None)
.map_err(|(_, e)| e)?
.id,
post_id: Post::from_id(
conn,
&act.announce_props.object_link::<Id>()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
user_id: User::from_id(
conn,
&act.announce_props.actor_link::<Id>()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
ap_url: act.object_props.id_string()?,
},
)?;
res.notify(&c.conn)?;
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, &PlumeRocket> for Reshare {
impl AsObject<User, Undo, &DbConn> for Reshare {
type Error = Error;
type Output = ();
fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
let conn = &*c.conn;
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
if actor.id == self.user_id {
diesel::delete(&self).execute(conn)?;
diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any
if let Ok(notif) = Notification::find(&conn, notification_kind::RESHARE, self.id) {
diesel::delete(&notif).execute(conn)?;
if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) {
diesel::delete(&notif).execute(&**conn)?;
}
Ok(())
+4 -4
View File
@@ -11,14 +11,13 @@ use std::{
borrow::{Borrow, Cow},
fmt::{self, Display},
io::Write,
iter,
ops::Deref,
};
lazy_static! {
static ref CLEAN: Builder<'static> = {
let mut b = Builder::new();
b.add_generic_attributes(iter::once("id"))
b.add_generic_attributes(&["id", "dir"])
.add_tags(&["iframe", "video", "audio", "label", "input"])
.id_prefix(Some("postcontent-"))
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
@@ -82,6 +81,7 @@ lazy_static! {
};
}
#[allow(clippy::unnecessary_wraps)]
fn url_add_prefix(url: &str) -> Option<Cow<'_, str>> {
if url.starts_with('#') && !url.starts_with("#postcontent-") {
//if start with an #
@@ -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(),
}
}
@@ -156,7 +156,7 @@ impl<'de> Deserialize<'de> for SafeString {
where
D: Deserializer<'de>,
{
Ok(deserializer.deserialize_string(SafeStringVisitor)?)
deserializer.deserialize_string(SafeStringVisitor)
}
}
+11
View File
@@ -73,6 +73,7 @@ table! {
user_id -> Int4,
}
}
table! {
email_blocklist(id){
id -> Int4,
@@ -83,6 +84,15 @@ table! {
}
}
table! {
email_signups (id) {
id -> Int4,
email -> Varchar,
token -> Varchar,
expiration_date -> Timestamp,
}
}
table! {
follows (id) {
id -> Int4,
@@ -306,6 +316,7 @@ allow_tables_to_appear_in_same_query!(
blogs,
comments,
comment_seers,
email_signups,
follows,
instances,
likes,
+214
View File
@@ -0,0 +1,214 @@
use super::Searcher;
use crate::{db_conn::DbPool, posts::PostEvent, ACTOR_SYS, POST_CHAN};
use riker::actors::{Actor, ActorFactoryArgs, ActorRefFactory, Context, Sender, Subscribe, Tell};
use std::sync::Arc;
use std::thread::sleep;
use std::time::Duration;
use tracing::error;
pub struct SearchActor {
searcher: Arc<Searcher>,
conn: DbPool,
}
impl SearchActor {
pub fn init(searcher: Arc<Searcher>, conn: DbPool) {
let actor = ACTOR_SYS
.actor_of_args::<SearchActor, _>("search", (searcher, conn))
.expect("Failed to initialize searcher actor");
POST_CHAN.tell(
Subscribe {
actor: Box::new(actor),
topic: "*".into(),
},
None,
)
}
}
impl Actor for SearchActor {
type Msg = PostEvent;
fn recv(&mut self, _ctx: &Context<Self::Msg>, msg: Self::Msg, _sender: Sender) {
use PostEvent::*;
// Wait for transaction commited
sleep(Duration::from_millis(500));
match msg {
PostPublished(post) => {
let conn = self.conn.get();
match conn {
Ok(conn) => {
self.searcher
.add_document(&conn, &post)
.unwrap_or_else(|e| error!("{:?}", e));
}
_ => {
error!("Failed to get database connection");
}
}
}
PostUpdated(post) => {
let conn = self.conn.get();
match conn {
Ok(_) => {
self.searcher
.update_document(&conn.unwrap(), &post)
.unwrap_or_else(|e| error!("{:?}", e));
}
_ => {
error!("Failed to get database connection");
}
}
}
PostDeleted(post) => self.searcher.delete_document(&post),
}
}
}
impl ActorFactoryArgs<(Arc<Searcher>, DbPool)> for SearchActor {
fn create_args((searcher, conn): (Arc<Searcher>, DbPool)) -> Self {
Self { searcher, conn }
}
}
#[cfg(test)]
mod tests {
use crate::diesel::Connection;
use crate::{
blog_authors::{BlogAuthor, NewBlogAuthor},
blogs::{Blog, NewBlog},
db_conn::{DbPool, PragmaForeignKey},
instance::{Instance, NewInstance},
post_authors::{NewPostAuthor, PostAuthor},
posts::{NewPost, Post},
safe_string::SafeString,
search::{actor::SearchActor, tests::get_searcher, Query},
users::{NewUser, User},
Connection as Conn, CONFIG,
};
use diesel::r2d2::ConnectionManager;
use plume_common::utils::random_hex;
use std::str::FromStr;
use std::sync::Arc;
use std::thread::sleep;
use std::time::Duration;
#[test]
fn post_updated() {
// Need to commit so that searcher on another thread retrieve records.
// So, build DbPool instead of using DB_POOL for testing.
let manager = ConnectionManager::<Conn>::new(CONFIG.database_url.as_str());
let db_pool = DbPool::builder()
.connection_customizer(Box::new(PragmaForeignKey))
.build(manager)
.unwrap();
let searcher = Arc::new(get_searcher(&CONFIG.search_tokenizers));
SearchActor::init(searcher.clone(), db_pool.clone());
let conn = db_pool.clone().get().unwrap();
let title = random_hex()[..8].to_owned();
let (_instance, _user, blog) = fill_database(&conn);
let author = &blog.list_authors(&conn).unwrap()[0];
let post = Post::insert(
&conn,
NewPost {
blog_id: blog.id,
slug: title.clone(),
title: title.clone(),
content: SafeString::new(""),
published: true,
license: "CC-BY-SA".to_owned(),
ap_url: "".to_owned(),
creation_date: None,
subtitle: "".to_owned(),
source: "".to_owned(),
cover_id: None,
},
)
.unwrap();
PostAuthor::insert(
&conn,
NewPostAuthor {
post_id: post.id,
author_id: author.id,
},
)
.unwrap();
let post_id = post.id;
// Wait for searcher on another thread add document asynchronously
sleep(Duration::from_millis(700));
searcher.commit();
assert_eq!(
searcher.search_document(&conn, Query::from_str(&title).unwrap(), (0, 1))[0].id,
post_id
);
}
fn fill_database(conn: &Conn) -> (Instance, User, Blog) {
conn.transaction::<(Instance, User, Blog), diesel::result::Error, _>(|| {
let instance = Instance::insert(
conn,
NewInstance {
default_license: "CC-0-BY-SA".to_string(),
local: false,
long_description: SafeString::new("Good morning"),
long_description_html: "<p>Good morning</p>".to_string(),
short_description: SafeString::new("Hello"),
short_description_html: "<p>Hello</p>".to_string(),
name: random_hex().to_string(),
open_registrations: true,
public_domain: random_hex().to_string(),
},
)
.unwrap();
let user = User::insert(
conn,
NewUser {
username: random_hex().to_string(),
display_name: random_hex().to_string(),
outbox_url: random_hex().to_string(),
inbox_url: random_hex().to_string(),
summary: "".to_string(),
email: None,
hashed_password: None,
instance_id: instance.id,
ap_url: random_hex().to_string(),
private_key: None,
public_key: "".to_string(),
shared_inbox_url: None,
followers_endpoint: random_hex().to_string(),
avatar_id: None,
summary_html: SafeString::new(""),
role: 0,
fqn: random_hex().to_string(),
},
)
.unwrap();
let mut blog = NewBlog::default();
blog.instance_id = instance.id;
blog.actor_id = random_hex().to_string();
blog.ap_url = random_hex().to_string();
blog.inbox_url = random_hex().to_string();
blog.outbox_url = random_hex().to_string();
let blog = Blog::insert(conn, blog).unwrap();
BlogAuthor::insert(
conn,
NewBlogAuthor {
blog_id: blog.id,
author_id: user.id,
is_owner: true,
},
)
.unwrap();
Ok((instance, user, blog))
})
.unwrap()
}
}
+10 -11
View File
@@ -1,3 +1,4 @@
pub mod actor;
mod query;
mod searcher;
mod tokenizer;
@@ -7,12 +8,7 @@ pub use self::tokenizer::TokenizerKind;
#[cfg(test)]
pub(crate) mod tests {
use super::{Query, Searcher, TokenizerKind};
use diesel::Connection;
use plume_common::utils::random_hex;
use std::env::temp_dir;
use std::str::FromStr;
use super::{Query, Searcher};
use crate::{
blogs::tests::fill_database,
config::SearchTokenizerConfig,
@@ -22,6 +18,10 @@ pub(crate) mod tests {
tests::db,
CONFIG,
};
use diesel::Connection;
use plume_common::utils::random_hex;
use std::env::temp_dir;
use std::str::FromStr;
pub(crate) fn get_searcher(tokenizers: &SearchTokenizerConfig) -> Searcher {
let dir = temp_dir().join(&format!("plume-test-{}", random_hex()));
@@ -144,7 +144,6 @@ pub(crate) mod tests {
source: "".to_owned(),
cover_id: None,
},
&searcher,
)
.unwrap();
PostAuthor::insert(
@@ -155,7 +154,7 @@ pub(crate) mod tests {
},
)
.unwrap();
searcher.add_document(&conn, &post).unwrap();
searcher.commit();
assert_eq!(
searcher.search_document(conn, Query::from_str(&title).unwrap(), (0, 1))[0].id,
@@ -164,7 +163,8 @@ pub(crate) mod tests {
let newtitle = random_hex()[..8].to_owned();
post.title = newtitle.clone();
post.update(conn, &searcher).unwrap();
post.update(conn).unwrap();
searcher.update_document(conn, &post).unwrap();
searcher.commit();
assert_eq!(
searcher.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))[0].id,
@@ -174,7 +174,7 @@ pub(crate) mod tests {
.search_document(conn, Query::from_str(&title).unwrap(), (0, 1))
.is_empty());
post.delete(conn, &searcher).unwrap();
searcher.delete_document(&post);
searcher.commit();
assert!(searcher
.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))
@@ -213,7 +213,6 @@ pub(crate) mod tests {
source: "".to_owned(),
cover_id: None,
},
&searcher,
)
.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)
}
}
+56 -2
View File
@@ -1,15 +1,17 @@
use crate::{
config::SearchTokenizerConfig, instance::Instance, posts::Post, schema::posts,
search::query::PlumeQuery, tags::Tag, Connection, Result,
search::query::PlumeQuery, tags::Tag, Connection, Error, Result,
};
use chrono::Datelike;
use chrono::{Datelike, Utc};
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use itertools::Itertools;
use std::fs;
use std::{cmp, fs::create_dir_all, io, path::Path, sync::Mutex};
use tantivy::{
collector::TopDocs, directory::MmapDirectory, schema::*, Index, IndexReader, IndexWriter,
ReloadPolicy, TantivyError, Term,
};
use tracing::warn;
use whatlang::{detect as detect_lang, Lang};
#[derive(Debug)]
@@ -67,6 +69,58 @@ impl Searcher {
schema_builder.build()
}
pub fn open_or_recreate(path: &dyn AsRef<Path>, tokenizers: &SearchTokenizerConfig) -> Self {
let mut open_searcher = Self::open(path, tokenizers);
if let Err(Error::Search(SearcherError::InvalidIndexDataError)) = open_searcher {
if Self::create(path, tokenizers).is_err() {
let backup_path = format!("{}.{}", path.as_ref().display(), Utc::now().timestamp());
let backup_path = Path::new(&backup_path);
fs::rename(path, backup_path)
.expect("main: error on backing up search index directory for recreating");
if Self::create(path, tokenizers).is_ok() {
if fs::remove_dir_all(backup_path).is_err() {
warn!(
"error on removing backup directory: {}. it remains",
backup_path.display()
);
}
} else {
panic!("main: error on recreating search index in new index format. remove search index and run `plm search init` manually");
}
}
open_searcher = Self::open(path, tokenizers);
}
match open_searcher {
Ok(s) => s,
Err(Error::Search(e)) => match e {
SearcherError::WriteLockAcquisitionError => panic!(
r#"
Your search index is locked. Plume can't start. To fix this issue
make sure no other Plume instance is started, and run:
plm search unlock
Then try to restart Plume.
"#
),
SearcherError::IndexOpeningError => panic!(
r#"
Plume was unable to open the search index. If you created the index
before, make sure to run Plume in the same directory it was created in, or
to set SEARCH_INDEX accordingly. If you did not yet create the search
index, run this command:
plm search init
Then try to restart Plume
"#
),
e => Err(e).unwrap(),
},
_ => panic!("Unexpected error while opening search index"),
}
}
pub fn create(path: &dyn AsRef<Path>, tokenizers: &SearchTokenizerConfig) -> Result<Self> {
let schema = Self::schema();
+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(()),
}
}
}
+147 -135
View File
@@ -1,8 +1,9 @@
use crate::{
db_conn::DbConn,
lists::List,
posts::Post,
schema::{posts, timeline, timeline_definition},
Connection, Error, PlumeRocket, Result,
Connection, Error, Result,
};
use diesel::{self, BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl};
use std::ops::Deref;
@@ -208,20 +209,23 @@ impl Timeline {
.map_err(Error::from)
}
pub fn add_to_all_timelines(rocket: &PlumeRocket, post: &Post, kind: Kind<'_>) -> Result<()> {
pub fn add_to_all_timelines(conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<()> {
let timelines = timeline_definition::table
.load::<Self>(rocket.conn.deref())
.load::<Self>(conn.deref())
.map_err(Error::from)?;
for t in timelines {
if t.matches(rocket, post, kind)? {
t.add_post(&rocket.conn, post)?;
if t.matches(conn, post, kind)? {
t.add_post(conn, post)?;
}
}
Ok(())
}
pub fn add_post(&self, conn: &Connection, post: &Post) -> Result<()> {
if self.includes_post(conn, post)? {
return Ok(());
}
diesel::insert_into(timeline::table)
.values(TimelineEntry {
post_id: post.id,
@@ -231,9 +235,19 @@ impl Timeline {
Ok(())
}
pub fn matches(&self, rocket: &PlumeRocket, post: &Post, kind: Kind<'_>) -> Result<bool> {
pub fn matches(&self, conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<bool> {
let query = TimelineQuery::parse(&self.query)?;
query.matches(rocket, self, post, kind)
query.matches(conn, self, post, kind)
}
fn includes_post(&self, conn: &Connection, post: &Post) -> Result<bool> {
diesel::dsl::select(diesel::dsl::exists(
timeline::table
.filter(timeline::timeline_id.eq(self.id))
.filter(timeline::post_id.eq(post.id)),
))
.get_result(conn)
.map_err(Error::from)
}
}
@@ -248,7 +262,7 @@ mod tests {
posts::NewPost,
safe_string::SafeString,
tags::Tag,
tests::{db, rockets},
tests::db,
users::tests as userTests,
};
use diesel::Connection;
@@ -257,63 +271,69 @@ mod tests {
fn test_timeline() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let users = userTests::fill_database(conn);
let users = userTests::fill_database(&conn);
let mut tl1_u1 = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"my timeline".to_owned(),
"all".to_owned(),
)
.unwrap();
List::new(conn, "languages I speak", Some(&users[1]), ListType::Prefix).unwrap();
List::new(
&conn,
"languages I speak",
Some(&users[1]),
ListType::Prefix,
)
.unwrap();
let tl2_u1 = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"another timeline".to_owned(),
"followed".to_owned(),
)
.unwrap();
let tl1_u2 = Timeline::new_for_user(
conn,
&conn,
users[1].id,
"english posts".to_owned(),
"lang in \"languages I speak\"".to_owned(),
)
.unwrap();
let tl1_instance = Timeline::new_for_instance(
conn,
&conn,
"english posts".to_owned(),
"license in [cc]".to_owned(),
)
.unwrap();
assert_eq!(tl1_u1, Timeline::get(conn, tl1_u1.id).unwrap());
assert_eq!(tl1_u1, Timeline::get(&conn, tl1_u1.id).unwrap());
assert_eq!(
tl2_u1,
Timeline::find_for_user_by_name(conn, Some(users[0].id), "another timeline")
Timeline::find_for_user_by_name(&conn, Some(users[0].id), "another timeline")
.unwrap()
);
assert_eq!(
tl1_instance,
Timeline::find_for_user_by_name(conn, None, "english posts").unwrap()
Timeline::find_for_user_by_name(&conn, None, "english posts").unwrap()
);
let tl_u1 = Timeline::list_for_user(conn, Some(users[0].id)).unwrap();
let tl_u1 = Timeline::list_for_user(&conn, Some(users[0].id)).unwrap();
assert_eq!(3, tl_u1.len()); // it is not 2 because there is a "Your feed" tl created for each user automatically
assert!(tl_u1.iter().fold(false, |res, tl| { res || *tl == tl1_u1 }));
assert!(tl_u1.iter().fold(false, |res, tl| { res || *tl == tl2_u1 }));
let tl_instance = Timeline::list_for_user(conn, None).unwrap();
let tl_instance = Timeline::list_for_user(&conn, None).unwrap();
assert_eq!(3, tl_instance.len()); // there are also the local and federated feed by default
assert!(tl_instance
.iter()
.fold(false, |res, tl| { res || *tl == tl1_instance }));
tl1_u1.name = "My Super TL".to_owned();
let new_tl1_u2 = tl1_u2.update(conn).unwrap();
let new_tl1_u2 = tl1_u2.update(&conn).unwrap();
let tl_u2 = Timeline::list_for_user(conn, Some(users[1].id)).unwrap();
let tl_u2 = Timeline::list_for_user(&conn, Some(users[1].id)).unwrap();
assert_eq!(2, tl_u2.len()); // same here
assert!(tl_u2
.iter()
@@ -327,48 +347,48 @@ mod tests {
fn test_timeline_creation_error() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let users = userTests::fill_database(conn);
let users = userTests::fill_database(&conn);
assert!(Timeline::new_for_user(
conn,
&conn,
users[0].id,
"my timeline".to_owned(),
"invalid keyword".to_owned(),
)
.is_err());
assert!(Timeline::new_for_instance(
conn,
&conn,
"my timeline".to_owned(),
"invalid keyword".to_owned(),
)
.is_err());
assert!(Timeline::new_for_user(
conn,
&conn,
users[0].id,
"my timeline".to_owned(),
"author in non_existant_list".to_owned(),
)
.is_err());
assert!(Timeline::new_for_instance(
conn,
&conn,
"my timeline".to_owned(),
"lang in dont-exist".to_owned(),
)
.is_err());
List::new(conn, "friends", Some(&users[0]), ListType::User).unwrap();
List::new(conn, "idk", None, ListType::Blog).unwrap();
List::new(&conn, "friends", Some(&users[0]), ListType::User).unwrap();
List::new(&conn, "idk", None, ListType::Blog).unwrap();
assert!(Timeline::new_for_user(
conn,
&conn,
users[0].id,
"my timeline".to_owned(),
"blog in friends".to_owned(),
)
.is_err());
assert!(Timeline::new_for_instance(
conn,
&conn,
"my timeline".to_owned(),
"not author in idk".to_owned(),
)
@@ -380,13 +400,12 @@ mod tests {
#[test]
fn test_simple_match() {
let r = &rockets();
let conn = &r.conn;
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(conn);
let (users, blogs) = blogTests::fill_database(&conn);
let gnu_tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"GNU timeline".to_owned(),
"license in [AGPL, LGPL, GPL]".to_owned(),
@@ -394,7 +413,7 @@ mod tests {
.unwrap();
let gnu_post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug".to_string(),
@@ -408,13 +427,12 @@ mod tests {
source: "you must say GNU/Linux, not Linux!!!".to_string(),
cover_id: None,
},
&r.searcher,
)
.unwrap();
assert!(gnu_tl.matches(r, &gnu_post, Kind::Original).unwrap());
assert!(gnu_tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
let non_free_post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug2".to_string(),
@@ -428,10 +446,11 @@ mod tests {
source: "so is Microsoft".to_string(),
cover_id: None,
},
&r.searcher,
)
.unwrap();
assert!(!gnu_tl.matches(r, &non_free_post, Kind::Original).unwrap());
assert!(!gnu_tl
.matches(&conn, &non_free_post, Kind::Original)
.unwrap());
Ok(())
});
@@ -439,12 +458,11 @@ mod tests {
#[test]
fn test_complex_match() {
let r = &rockets();
let conn = &r.conn;
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(conn);
let (users, blogs) = blogTests::fill_database(&conn);
Follow::insert(
conn,
&conn,
NewFollow {
follower_id: users[0].id,
following_id: users[1].id,
@@ -454,11 +472,11 @@ mod tests {
.unwrap();
let fav_blogs_list =
List::new(conn, "fav_blogs", Some(&users[0]), ListType::Blog).unwrap();
fav_blogs_list.add_blogs(conn, &[blogs[0].id]).unwrap();
List::new(&conn, "fav_blogs", Some(&users[0]), ListType::Blog).unwrap();
fav_blogs_list.add_blogs(&conn, &[blogs[0].id]).unwrap();
let my_tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"My timeline".to_owned(),
"blog in fav_blogs and not has_cover or local and followed exclude likes"
@@ -467,7 +485,7 @@ mod tests {
.unwrap();
let post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "about-linux".to_string(),
@@ -481,13 +499,12 @@ mod tests {
subtitle: "".to_string(),
cover_id: None,
},
&r.searcher,
)
.unwrap();
assert!(my_tl.matches(r, &post, Kind::Original).unwrap()); // matches because of "blog in fav_blogs" (and there is no cover)
assert!(my_tl.matches(&conn, &post, Kind::Original).unwrap()); // matches because of "blog in fav_blogs" (and there is no cover)
let post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[1].id,
slug: "about-linux-2".to_string(),
@@ -503,10 +520,9 @@ mod tests {
subtitle: "".to_string(),
cover_id: None,
},
&r.searcher,
)
.unwrap();
assert!(!my_tl.matches(r, &post, Kind::Like(&users[1])).unwrap());
assert!(!my_tl.matches(&conn, &post, Kind::Like(&users[1])).unwrap());
Ok(())
});
@@ -514,20 +530,19 @@ mod tests {
#[test]
fn test_add_to_all_timelines() {
let r = &rockets();
let conn = &r.conn;
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(conn);
let (users, blogs) = blogTests::fill_database(&conn);
let gnu_tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"GNU timeline".to_owned(),
"license in [AGPL, LGPL, GPL]".to_owned(),
)
.unwrap();
let non_gnu_tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"Stallman disapproved timeline".to_owned(),
"not license in [AGPL, LGPL, GPL]".to_owned(),
@@ -535,7 +550,7 @@ mod tests {
.unwrap();
let gnu_post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug".to_string(),
@@ -549,12 +564,11 @@ mod tests {
source: "you must say GNU/Linux, not Linux!!!".to_string(),
cover_id: None,
},
&r.searcher,
)
.unwrap();
let non_free_post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug2".to_string(),
@@ -568,17 +582,16 @@ mod tests {
source: "so is Microsoft".to_string(),
cover_id: None,
},
&r.searcher,
)
.unwrap();
Timeline::add_to_all_timelines(r, &gnu_post, Kind::Original).unwrap();
Timeline::add_to_all_timelines(r, &non_free_post, Kind::Original).unwrap();
Timeline::add_to_all_timelines(&conn, &gnu_post, Kind::Original).unwrap();
Timeline::add_to_all_timelines(&conn, &non_free_post, Kind::Original).unwrap();
let res = gnu_tl.get_latest(conn, 2).unwrap();
let res = gnu_tl.get_latest(&conn, 2).unwrap();
assert_eq!(res.len(), 1);
assert_eq!(res[0].id, gnu_post.id);
let res = non_gnu_tl.get_latest(conn, 2).unwrap();
let res = non_gnu_tl.get_latest(&conn, 2).unwrap();
assert_eq!(res.len(), 1);
assert_eq!(res[0].id, non_free_post.id);
@@ -588,13 +601,12 @@ mod tests {
#[test]
fn test_matches_lists_direct() {
let r = &rockets();
let conn = &r.conn;
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(conn);
let (users, blogs) = blogTests::fill_database(&conn);
let gnu_post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug".to_string(),
@@ -608,65 +620,66 @@ mod tests {
source: "you must say GNU/Linux, not Linux!!!".to_string(),
cover_id: None,
},
&r.searcher,
)
.unwrap();
gnu_post
.update_tags(conn, vec![Tag::build_activity("free".to_owned()).unwrap()])
.update_tags(&conn, vec![Tag::build_activity("free".to_owned()).unwrap()])
.unwrap();
PostAuthor::insert(
conn,
&conn,
NewPostAuthor {
post_id: gnu_post.id,
author_id: blogs[0].list_authors(conn).unwrap()[0].id,
author_id: blogs[0].list_authors(&conn).unwrap()[0].id,
},
)
.unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"blog timeline".to_owned(),
format!("blog in [{}]", blogs[0].fqn),
)
.unwrap();
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"blog timeline".to_owned(),
"blog in [no_one@nowhere]".to_owned(),
)
.unwrap();
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"author timeline".to_owned(),
format!(
"author in [{}]",
blogs[0].list_authors(conn).unwrap()[0].fqn
blogs[0].list_authors(&conn).unwrap()[0].fqn
),
)
.unwrap();
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"author timeline".to_owned(),
format!("author in [{}]", users[2].fqn),
)
.unwrap();
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
assert!(tl.matches(r, &gnu_post, Kind::Reshare(&users[2])).unwrap());
assert!(!tl.matches(r, &gnu_post, Kind::Like(&users[2])).unwrap());
tl.delete(conn).unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
assert!(tl
.matches(&conn, &gnu_post, Kind::Reshare(&users[2]))
.unwrap());
assert!(!tl.matches(&conn, &gnu_post, Kind::Like(&users[2])).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"author timeline".to_owned(),
format!(
@@ -675,48 +688,50 @@ mod tests {
),
)
.unwrap();
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
assert!(!tl.matches(r, &gnu_post, Kind::Reshare(&users[2])).unwrap());
assert!(tl.matches(r, &gnu_post, Kind::Like(&users[2])).unwrap());
tl.delete(conn).unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
assert!(!tl
.matches(&conn, &gnu_post, Kind::Reshare(&users[2]))
.unwrap());
assert!(tl.matches(&conn, &gnu_post, Kind::Like(&users[2])).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"tag timeline".to_owned(),
"tags in [free]".to_owned(),
)
.unwrap();
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"tag timeline".to_owned(),
"tags in [private]".to_owned(),
)
.unwrap();
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"english timeline".to_owned(),
"lang in [en]".to_owned(),
)
.unwrap();
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"franco-italian timeline".to_owned(),
"lang in [fr, it]".to_owned(),
)
.unwrap();
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
Ok(())
});
@@ -726,12 +741,12 @@ mod tests {
#[test]
fn test_matches_lists_saved() {
let r = &rockets();
let conn = &r.conn;
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(conn);
let (users, blogs) = blogTests::fill_database(&conn);
let gnu_post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug".to_string(),
@@ -745,11 +760,10 @@ mod tests {
source: "you must say GNU/Linux, not Linux!!!".to_string(),
cover_id: None,
},
&r.searcher,
)
.unwrap();
gnu_post.update_tags(conn, vec![Tag::build_activity("free".to_owned()).unwrap()]).unwrap();
PostAuthor::insert(conn, NewPostAuthor {post_id: gnu_post.id, author_id: blogs[0].list_authors(conn).unwrap()[0].id}).unwrap();
gnu_post.update_tags(&conn, vec![Tag::build_activity("free".to_owned()).unwrap()]).unwrap();
PostAuthor::insert(&conn, NewPostAuthor {post_id: gnu_post.id, author_id: blogs[0].list_authors(&conn).unwrap()[0].id}).unwrap();
unimplemented!();
@@ -759,13 +773,12 @@ mod tests {
#[test]
fn test_matches_keyword() {
let r = &rockets();
let conn = &r.conn;
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(conn);
let (users, blogs) = blogTests::fill_database(&conn);
let gnu_post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug".to_string(),
@@ -779,66 +792,65 @@ mod tests {
source: "you must say GNU/Linux, not Linux!!!".to_string(),
cover_id: None,
},
&r.searcher,
)
.unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"Linux title".to_owned(),
"title contains Linux".to_owned(),
)
.unwrap();
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"Microsoft title".to_owned(),
"title contains Microsoft".to_owned(),
)
.unwrap();
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"Linux subtitle".to_owned(),
"subtitle contains Stallman".to_owned(),
)
.unwrap();
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"Microsoft subtitle".to_owned(),
"subtitle contains Nadella".to_owned(),
)
.unwrap();
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"Linux content".to_owned(),
"content contains Linux".to_owned(),
)
.unwrap();
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"Microsoft content".to_owned(),
"subtitle contains Windows".to_owned(),
)
.unwrap();
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
Ok(())
});
+80 -68
View File
@@ -1,11 +1,12 @@
use crate::{
blogs::Blog,
db_conn::DbConn,
lists::{self, ListType},
posts::Post,
tags::Tag,
timeline::Timeline,
users::User,
PlumeRocket, Result,
Result,
};
use plume_common::activity_pub::inbox::AsActor;
use whatlang::{self, Lang};
@@ -17,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)]
@@ -160,19 +155,19 @@ enum TQ<'a> {
impl<'a> TQ<'a> {
fn matches(
&self,
rocket: &PlumeRocket,
conn: &DbConn,
timeline: &Timeline,
post: &Post,
kind: Kind<'_>,
) -> Result<bool> {
match self {
TQ::Or(inner) => inner.iter().try_fold(false, |s, e| {
e.matches(rocket, timeline, post, kind).map(|r| s || r)
e.matches(conn, timeline, post, kind).map(|r| s || r)
}),
TQ::And(inner) => inner.iter().try_fold(true, |s, e| {
e.matches(rocket, timeline, post, kind).map(|r| s && r)
e.matches(conn, timeline, post, kind).map(|r| s && r)
}),
TQ::Arg(inner, invert) => Ok(inner.matches(rocket, timeline, post, kind)? ^ invert),
TQ::Arg(inner, invert) => Ok(inner.matches(conn, timeline, post, kind)? ^ invert),
}
}
@@ -205,15 +200,15 @@ enum Arg<'a> {
impl<'a> Arg<'a> {
pub fn matches(
&self,
rocket: &PlumeRocket,
conn: &DbConn,
timeline: &Timeline,
post: &Post,
kind: Kind<'_>,
) -> Result<bool> {
match self {
Arg::In(t, l) => t.matches(rocket, timeline, post, l, kind),
Arg::In(t, l) => t.matches(conn, timeline, post, l, kind),
Arg::Contains(t, v) => t.matches(post, v),
Arg::Boolean(t) => t.matches(rocket, timeline, post, kind),
Arg::Boolean(t) => t.matches(conn, timeline, post, kind),
}
}
}
@@ -230,7 +225,7 @@ enum WithList {
impl WithList {
pub fn matches(
&self,
rocket: &PlumeRocket,
conn: &DbConn,
timeline: &Timeline,
post: &Post,
list: &List<'_>,
@@ -238,39 +233,34 @@ impl WithList {
) -> Result<bool> {
match list {
List::List(name) => {
let list =
lists::List::find_for_user_by_name(&rocket.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(&rocket.conn, post.blog_id)
}
(WithList::Blog, ListType::Blog) => list.contains_blog(conn, post.blog_id),
(WithList::Author { boosts, likes }, ListType::User) => match kind {
Kind::Original => Ok(list
.list_users(&rocket.conn)?
.list_users(conn)?
.iter()
.any(|a| post.is_author(&rocket.conn, a.id).unwrap_or(false))),
.any(|a| post.is_author(conn, a.id).unwrap_or(false))),
Kind::Reshare(u) => {
if *boosts {
list.contains_user(&rocket.conn, u.id)
list.contains_user(conn, u.id)
} else {
Ok(false)
}
}
Kind::Like(u) => {
if *likes {
list.contains_user(&rocket.conn, u.id)
list.contains_user(conn, u.id)
} else {
Ok(false)
}
}
},
(WithList::License, ListType::Word) => {
list.contains_word(&rocket.conn, &post.license)
}
(WithList::License, ListType::Word) => list.contains_word(conn, &post.license),
(WithList::Tags, ListType::Word) => {
let tags = Tag::for_post(&rocket.conn, post.id)?;
let tags = Tag::for_post(conn, post.id)?;
Ok(list
.list_words(&rocket.conn)?
.list_words(conn)?
.iter()
.any(|s| tags.iter().any(|t| s == &t.tag)))
}
@@ -285,7 +275,7 @@ impl WithList {
})
.unwrap_or(Lang::Eng)
.name();
list.contains_prefix(&rocket.conn, lang)
list.contains_prefix(conn, lang)
}
(_, _) => Err(QueryError::RuntimeError(format!(
"The list '{}' is of the wrong type for this usage",
@@ -297,13 +287,13 @@ impl WithList {
List::Array(list) => match self {
WithList::Blog => Ok(list
.iter()
.filter_map(|b| Blog::find_by_fqn(rocket, b).ok())
.filter_map(|b| Blog::find_by_fqn(conn, b).ok())
.any(|b| b.id == post.blog_id)),
WithList::Author { boosts, likes } => match kind {
Kind::Original => Ok(list
.iter()
.filter_map(|a| User::find_by_fqn(rocket, a).ok())
.any(|a| post.is_author(&rocket.conn, a.id).unwrap_or(false))),
.filter_map(|a| User::find_by_fqn(&*conn, a).ok())
.any(|a| post.is_author(conn, a.id).unwrap_or(false))),
Kind::Reshare(u) => {
if *boosts {
Ok(list.iter().any(|user| &u.fqn == user))
@@ -321,7 +311,7 @@ impl WithList {
},
WithList::License => Ok(list.iter().any(|s| s == &post.license)),
WithList::Tags => {
let tags = Tag::for_post(&rocket.conn, post.id)?;
let tags = Tag::for_post(conn, post.id)?;
Ok(list.iter().any(|s| tags.iter().any(|t| s == &t.tag)))
}
WithList::Lang => {
@@ -371,7 +361,7 @@ enum Bool {
impl Bool {
pub fn matches(
&self,
rocket: &PlumeRocket,
conn: &DbConn,
timeline: &Timeline,
post: &Post,
kind: Kind<'_>,
@@ -384,21 +374,19 @@ impl Bool {
let user = timeline.user_id.unwrap();
match kind {
Kind::Original => post
.get_authors(&rocket.conn)?
.get_authors(conn)?
.iter()
.try_fold(false, |s, a| {
a.is_followed_by(&rocket.conn, user).map(|r| s || r)
}),
.try_fold(false, |s, a| a.is_followed_by(conn, user).map(|r| s || r)),
Kind::Reshare(u) => {
if *boosts {
u.is_followed_by(&rocket.conn, user)
u.is_followed_by(conn, user)
} else {
Ok(false)
}
}
Kind::Like(u) => {
if *likes {
u.is_followed_by(&rocket.conn, user)
u.is_followed_by(conn, user)
} else {
Ok(false)
}
@@ -406,7 +394,7 @@ impl Bool {
}
}
Bool::HasCover => Ok(post.cover_id.is_some()),
Bool::Local => Ok(post.get_blog(&rocket.conn)?.is_local() && kind == Kind::Original),
Bool::Local => Ok(post.get_blog(conn)?.is_local() && kind == Kind::Original),
Bool::All => Ok(kind == Kind::Original),
}
}
@@ -420,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() {
@@ -442,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() {
@@ -469,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),
@@ -490,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 {
@@ -504,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
}
@@ -535,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(
@@ -561,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,
@@ -583,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 ','")),
}
@@ -607,17 +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::new();
res.push(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..];
}
@@ -642,12 +654,12 @@ impl<'a> TimelineQuery<'a> {
pub fn matches(
&self,
rocket: &PlumeRocket,
conn: &DbConn,
timeline: &Timeline,
post: &Post,
kind: Kind<'_>,
) -> Result<bool> {
self.0.matches(rocket, timeline, post, kind)
self.0.matches(conn, timeline, post, kind)
}
pub fn list_used_lists(&self) -> Vec<(String, ListType)> {
+161 -158
View File
@@ -1,8 +1,8 @@
use crate::{
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, config::CONFIG, db_conn::DbConn,
follows::Follow, instance::*, medias::Media, notifications::Notification,
post_authors::PostAuthor, posts::Post, safe_string::SafeString, schema::users,
search::Searcher, timeline::Timeline, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE,
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, db_conn::DbConn, follows::Follow,
instance::*, medias::Media, notifications::Notification, post_authors::PostAuthor, posts::Post,
safe_string::SafeString, schema::users, timeline::Timeline, Connection, Error, Result,
UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN,
};
use activitypub::{
activity::Delete,
@@ -11,7 +11,6 @@ use activitypub::{
object::{Image, Tombstone},
Activity, CustomObject, Endpoint,
};
use bcrypt;
use chrono::{NaiveDateTime, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
use ldap3::{LdapConn, Scope, SearchEntry};
@@ -23,25 +22,22 @@ use openssl::{
};
use plume_common::{
activity_pub::{
ap_accept_header,
inbox::{AsActor, AsObject, FromId},
sign::{gen_keypair, Signer},
request::get,
sign::{gen_keypair, Error as SignError, Result as SignResult, Signer},
ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
},
utils,
};
use reqwest::{
header::{HeaderValue, ACCEPT},
ClientBuilder,
};
use riker::actors::{Publish, Tell};
use rocket::{
outcome::IntoOutcome,
request::{self, FromRequest, Request},
};
use serde_json;
use std::{
cmp::PartialEq,
hash::{Hash, Hasher},
sync::Arc,
};
use url::Url;
use webfinger::*;
@@ -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>,
@@ -131,14 +129,14 @@ impl User {
.map_err(Error::from)
}
pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
pub fn delete(&self, conn: &Connection) -> Result<()> {
use crate::schema::post_authors;
for blog in Blog::find_for_author(conn, self)?
.iter()
.filter(|b| b.count_authors(conn).map(|c| c <= 1).unwrap_or(false))
{
blog.delete(conn, searcher)?;
blog.delete(conn)?;
}
// delete the posts if they is the only author
let all_their_posts_ids: Vec<i32> = post_authors::table
@@ -158,7 +156,7 @@ impl User {
.unwrap_or(&0)
> &0;
if !has_other_authors {
Post::get(conn, post_id)?.delete(conn, searcher)?;
Post::get(conn, post_id)?.delete(conn)?;
}
}
@@ -188,29 +186,51 @@ impl User {
users::table
.filter(users::instance_id.eq(Instance::get_local()?.id))
.count()
.get_result(conn)
.get_result(&*conn)
.map_err(Error::from)
}
pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<User> {
pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result<User> {
let from_db = users::table
.filter(users::fqn.eq(fqn))
.first(&*c.conn)
.first(&**conn)
.optional()?;
if let Some(from_db) = from_db {
Ok(from_db)
} else {
User::fetch_from_webfinger(c, fqn)
User::fetch_from_webfinger(conn, fqn)
}
}
fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<User> {
/**
* TODO: Should create user record with normalized(lowercased) email
*/
pub fn email_used(conn: &DbConn, email: &str) -> Result<bool> {
use diesel::dsl::{exists, select};
select(exists(
users::table
.filter(users::instance_id.eq(Instance::get_local()?.id))
.filter(users::email.eq(email))
.or_filter(users::email.eq(email.to_ascii_lowercase())),
))
.get_result(&**conn)
.map_err(Error::from)
}
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<User> {
let link = resolve(acct.to_owned(), true)?
.links
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger)?;
User::from_id(c, link.href.as_ref()?, None).map_err(|(_, e)| e)
User::from_id(
conn,
link.href.as_ref().ok_or(Error::Webfinger)?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)
}
pub fn fetch_remote_interact_uri(acct: &str) -> Result<String> {
@@ -223,20 +243,7 @@ impl User {
}
fn fetch(url: &str) -> Result<CustomPerson> {
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(url)
.header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?;
let text = &res.text()?;
// without this workaround, publicKey is not correctly deserialized
let ap_sign = serde_json::from_str::<ApSignature>(text)?;
@@ -245,8 +252,8 @@ impl User {
Ok(json)
}
pub fn fetch_from_url(c: &PlumeRocket, url: &str) -> Result<User> {
User::fetch(url).and_then(|json| User::from_activity(c, json))
pub fn fetch_from_url(conn: &DbConn, url: &str) -> Result<User> {
User::fetch(url).and_then(|json| User::from_activity(conn, json))
}
pub fn refetch(&self, conn: &Connection) -> Result<()> {
@@ -255,10 +262,10 @@ impl User {
conn,
json.object
.object_props
.icon_image()?
.icon_image()? // FIXME: Fails when icon is not set
.object_props
.url_string()?,
&self,
self,
)
.ok();
@@ -427,12 +434,12 @@ impl User {
let last = &format!(
"{}?page={}",
&self.outbox_url,
self.get_activities_count(&conn) / i64::from(ITEMS_PER_PAGE) + 1
self.get_activities_count(conn) / i64::from(ITEMS_PER_PAGE) + 1
);
coll.collection_props.set_first_link(Id::new(first))?;
coll.collection_props.set_last_link(Id::new(last))?;
coll.collection_props
.set_total_items_u64(self.get_activities_count(&conn) as u64)?;
.set_total_items_u64(self.get_activities_count(conn) as u64)?;
Ok(ActivityStream::new(coll))
}
pub fn outbox_page(
@@ -441,7 +448,7 @@ impl User {
(min, max): (i32, i32),
) -> Result<ActivityStream<OrderedCollectionPage>> {
let acts = self.get_activities_page(conn, (min, max))?;
let n_acts = self.get_activities_count(&conn);
let n_acts = self.get_activities_count(conn);
let mut coll = OrderedCollectionPage::default();
if n_acts - i64::from(min) >= i64::from(ITEMS_PER_PAGE) {
coll.collection_page_props.set_next_link(Id::new(&format!(
@@ -463,20 +470,7 @@ impl User {
Ok(ActivityStream::new(coll))
}
fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> {
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(url)
.header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?;
let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?;
let items = json["items"]
@@ -486,27 +480,15 @@ impl User {
.filter_map(|j| serde_json::from_value(j.clone()).ok())
.collect::<Vec<T>>();
let next = match json.get("next") {
Some(x) => Some(x.as_str().unwrap().to_owned()),
None => None,
};
let next = json.get("next").map(|x| x.as_str().unwrap().to_owned());
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") {
@@ -516,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;
@@ -538,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"]
@@ -723,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)
}
@@ -832,11 +805,7 @@ impl User {
pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> {
Ok(Webfinger {
subject: format!(
"acct:{}@{}",
self.username,
self.get_instance(conn)?.public_domain
),
subject: format!("acct:{}", self.acct_authority(conn)?),
aliases: vec![self.ap_url.clone()],
links: vec![
Link {
@@ -874,6 +843,14 @@ impl User {
})
}
pub fn acct_authority(&self, conn: &Connection) -> Result<String> {
Ok(format!(
"{}@{}",
self.username,
self.get_instance(conn)?.public_domain
))
}
pub fn set_avatar(&self, conn: &Connection, id: i32) -> Result<()> {
diesel::update(self)
.set(users::avatar_id.eq(id))
@@ -893,6 +870,21 @@ impl User {
self.fqn.clone()
}
}
pub fn remote_user_found(&self) {
tracing::trace!("{:?}", self);
self.publish_remote_user_found();
}
fn publish_remote_user_found(&self) {
USER_CHAN.tell(
Publish {
msg: RemoteUserFound(Arc::new(self.clone())),
topic: "user.remote_user_found".into(),
},
None,
)
}
}
impl<'a, 'r> FromRequest<'a, 'r> for User {
@@ -917,20 +909,20 @@ impl IntoId for User {
impl Eq for User {}
impl FromId<PlumeRocket> for User {
impl FromId<DbConn> for User {
type Error = Error;
type Object = CustomPerson;
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Self::find_by_ap_url(&c.conn, id)
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id)
}
fn from_activity(c: &PlumeRocket, acct: CustomPerson) -> Result<Self> {
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 instance = Instance::find_by_domain(&c.conn, inst).or_else(|_| {
let inst = url.host_str().ok_or(Error::Url)?;
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
Instance::insert(
&c.conn,
conn,
NewInstance {
name: inst.to_owned(),
public_domain: inst.to_owned(),
@@ -959,7 +951,7 @@ impl FromId<PlumeRocket> for User {
};
let user = User::insert(
&c.conn,
conn,
NewUser {
display_name: acct
.object
@@ -1005,19 +997,23 @@ impl FromId<PlumeRocket> for User {
if let Ok(icon) = acct.object.object_props.icon_image() {
if let Ok(url) = icon.object_props.url_string() {
let avatar = Media::save_remote(&c.conn, url, &user);
let avatar = Media::save_remote(conn, url, &user);
if let Ok(avatar) = avatar {
user.set_avatar(&c.conn, avatar.id)?;
user.set_avatar(conn, avatar.id)?;
}
}
}
Ok(user)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl AsActor<&PlumeRocket> for User {
impl AsActor<&DbConn> for User {
fn get_inbox_url(&self) -> String {
self.inbox_url.clone()
}
@@ -1033,13 +1029,13 @@ impl AsActor<&PlumeRocket> for User {
}
}
impl AsObject<User, Delete, &PlumeRocket> for User {
impl AsObject<User, Delete, &DbConn> for User {
type Error = Error;
type Output = ();
fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
if self.id == actor.id {
self.delete(&c.conn, &c.searcher).map(|_| ())
self.delete(conn).map(|_| ())
} else {
Err(Error::Unauthorized)
}
@@ -1047,24 +1043,22 @@ impl AsObject<User, Delete, &PlumeRocket> for User {
}
impl Signer for User {
type Error = Error;
fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url)
}
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = self.get_keypair()?;
fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
let key = self.get_keypair().map_err(|_| SignError())?;
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?;
signer.sign_to_vec().map_err(Error::from)
signer.sign_to_vec().map_err(SignError::from)
}
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?;
verifier.verify(&signature).map_err(Error::from)
verifier.verify(signature).map_err(SignError::from)
}
}
@@ -1105,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,
@@ -1128,14 +1122,27 @@ impl NewUser {
}
}
#[derive(Clone, Debug)]
pub enum UserEvent {
RemoteUserFound(Arc<User>),
}
impl From<UserEvent> for Arc<User> {
fn from(event: UserEvent) -> Self {
use UserEvent::*;
match event {
RemoteUserFound(user) => user,
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::{
config::CONFIG,
instance::{tests as instance_tests, Instance},
search::tests::get_searcher,
tests::{db, rockets},
tests::db,
Connection as Conn,
};
use diesel::Connection;
@@ -1177,12 +1184,11 @@ pub(crate) mod tests {
#[test]
fn find_by() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
fill_database(conn);
fill_database(&conn);
let test_user = NewUser::new_local(
conn,
&conn,
"test".to_owned(),
"test user".to_owned(),
Role::Normal,
@@ -1193,22 +1199,22 @@ pub(crate) mod tests {
.unwrap();
assert_eq!(
test_user.id,
User::find_by_name(conn, "test", Instance::get_local().unwrap().id)
User::find_by_name(&conn, "test", Instance::get_local().unwrap().id)
.unwrap()
.id
);
assert_eq!(
test_user.id,
User::find_by_fqn(&r, &test_user.fqn).unwrap().id
User::find_by_fqn(&conn, &test_user.fqn).unwrap().id
);
assert_eq!(
test_user.id,
User::find_by_email(conn, "test@example.com").unwrap().id
User::find_by_email(&conn, "test@example.com").unwrap().id
);
assert_eq!(
test_user.id,
User::find_by_ap_url(
conn,
&conn,
&format!(
"https://{}/@/{}/",
Instance::get_local().unwrap().public_domain,
@@ -1226,13 +1232,11 @@ pub(crate) mod tests {
fn delete() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let inserted = fill_database(conn);
let inserted = fill_database(&conn);
assert!(User::get(conn, inserted[0].id).is_ok());
inserted[0]
.delete(conn, &get_searcher(&CONFIG.search_tokenizers))
.unwrap();
assert!(User::get(conn, inserted[0].id).is_err());
assert!(User::get(&conn, inserted[0].id).is_ok());
inserted[0].delete(&conn).unwrap();
assert!(User::get(&conn, inserted[0].id).is_err());
Ok(())
});
}
@@ -1241,20 +1245,20 @@ pub(crate) mod tests {
fn admin() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let inserted = fill_database(conn);
let inserted = fill_database(&conn);
let local_inst = Instance::get_local().unwrap();
let mut i = 0;
while local_inst.has_admin(conn).unwrap() {
while local_inst.has_admin(&conn).unwrap() {
assert!(i < 100); //prevent from looping indefinitelly
local_inst
.main_admin(conn)
.main_admin(&conn)
.unwrap()
.set_role(conn, Role::Normal)
.set_role(&conn, Role::Normal)
.unwrap();
i += 1;
}
inserted[0].set_role(conn, Role::Admin).unwrap();
assert_eq!(inserted[0].id, local_inst.main_admin(conn).unwrap().id);
inserted[0].set_role(&conn, Role::Admin).unwrap();
assert_eq!(inserted[0].id, local_inst.main_admin(&conn).unwrap().id);
Ok(())
});
}
@@ -1263,9 +1267,9 @@ pub(crate) mod tests {
fn auth() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(conn);
fill_database(&conn);
let test_user = NewUser::new_local(
conn,
&conn,
"test".to_owned(),
"test user".to_owned(),
Role::Normal,
@@ -1276,10 +1280,10 @@ pub(crate) mod tests {
.unwrap();
assert_eq!(
User::login(conn, "test", "test_password").unwrap().id,
User::login(&conn, "test", "test_password").unwrap().id,
test_user.id
);
assert!(User::login(conn, "test", "other_password").is_err());
assert!(User::login(&conn, "test", "other_password").is_err());
Ok(())
});
}
@@ -1288,26 +1292,26 @@ pub(crate) mod tests {
fn get_local_page() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(conn);
fill_database(&conn);
let page = User::get_local_page(conn, (0, 2)).unwrap();
let page = User::get_local_page(&conn, (0, 2)).unwrap();
assert_eq!(page.len(), 2);
assert!(page[0].username <= page[1].username);
let mut last_username = User::get_local_page(conn, (0, 1)).unwrap()[0]
let mut last_username = User::get_local_page(&conn, (0, 1)).unwrap()[0]
.username
.clone();
for i in 1..User::count_local(conn).unwrap() as i32 {
let page = User::get_local_page(conn, (i, i + 1)).unwrap();
for i in 1..User::count_local(&conn).unwrap() as i32 {
let page = User::get_local_page(&conn, (i, i + 1)).unwrap();
assert_eq!(page.len(), 1);
assert!(last_username <= page[0].username);
last_username = page[0].username.clone();
}
assert_eq!(
User::get_local_page(conn, (0, User::count_local(conn).unwrap() as i32 + 10))
User::get_local_page(&conn, (0, User::count_local(&conn).unwrap() as i32 + 10))
.unwrap()
.len() as i64,
User::count_local(conn).unwrap()
User::count_local(&conn).unwrap()
);
Ok(())
});
@@ -1315,14 +1319,13 @@ pub(crate) mod tests {
#[test]
fn self_federation() {
let r = rockets();
let conn = &*r.conn;
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let users = fill_database(conn);
let users = fill_database(&conn);
let ap_repr = users[0].to_activity(conn).unwrap();
users[0].delete(conn, &*r.searcher).unwrap();
let user = User::from_activity(&r, ap_repr).unwrap();
let ap_repr = users[0].to_activity(&conn).unwrap();
users[0].delete(&conn).unwrap();
let user = User::from_activity(&conn, ap_repr).unwrap();
assert_eq!(user.username, users[0].username);
assert_eq!(user.display_name, users[0].display_name);
@@ -1333,7 +1336,7 @@ pub(crate) mod tests {
assert_eq!(user.public_key, users[0].public_key);
assert_eq!(user.shared_inbox_url, users[0].shared_inbox_url);
assert_eq!(user.followers_endpoint, users[0].followers_endpoint);
assert_eq!(user.avatar_url(conn), users[0].avatar_url(conn));
assert_eq!(user.avatar_url(&conn), users[0].avatar_url(&conn));
assert_eq!(user.fqn, users[0].fqn);
assert_eq!(user.summary_html, users[0].summary_html);
Ok(())
+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"
+8 -4
View File
@@ -10,12 +10,17 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=(n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? 2 : ((n == 3) ? 3 : ((n == 6) ? 4 : 5))));\n"
"Plural-Forms: nplurals=6; plural=(n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? "
"2 : ((n == 3) ? 3 : ((n == 6) ? 4 : 5))));\n"
"X-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: cy\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:114
msgid "Open the rich text editor"
msgstr ""
@@ -24,8 +29,8 @@ msgstr ""
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:147
msgid "Subtitle or summary"
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:154
@@ -55,4 +60,3 @@ msgstr ""
# plume-front/src/editor.rs:259
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: 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"

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