Merge branch 'master' into jmarthernandez-remove-karma-from-search

This commit is contained in:
Dessalines 2020-07-11 19:12:56 -04:00
commit 60288b2d06
136 changed files with 5844 additions and 2214 deletions

27
.github/ISSUE_TEMPLATE/BUG_REPORT.md vendored Normal file
View file

@ -0,0 +1,27 @@
---
name: "\U0001F41E Bug Report"
about: Create a report to help us improve Lemmy
title: ''
labels: bug
assignees: ''
---
Found a bug? Please fill out the sections below. 👍
### Issue Summary
A summary of the bug.
### Steps to Reproduce
1. (for example) I clicked login, and an endless spinner show up.
2. I tried to install lemmy via this guide, and I'm getting this error.
3. ...
### Technical details
* Please post your log: `sudo docker-compose logs > lemmy_log.out`.
* What OS are you trying to install lemmy on?
* Any browser console errors?

View file

@ -0,0 +1,42 @@
---
name: "\U0001F680 Feature request"
about: Suggest an idea for improving Lemmy
title: ''
labels: enhancement
assignees: ''
---
### Is your proposal related to a problem?
<!--
Provide a clear and concise description of what the problem is.
For example, "I'm always frustrated when..."
-->
(Write your answer here.)
### Describe the solution you'd like
<!--
Provide a clear and concise description of what you want to happen.
-->
(Describe your proposed solution here.)
### Describe alternatives you've considered
<!--
Let us know about other solutions you've tried or researched.
-->
(Write your answer here.)
### Additional context
<!--
Is there anything else you can add about the proposal?
You might want to link to related issues here, if you haven't already.
-->
(Write your answer here.)

10
.github/ISSUE_TEMPLATE/QUESTION.md vendored Normal file
View file

@ -0,0 +1,10 @@
---
name: "? Question"
about: General questions about Lemmy
title: ''
labels: question
assignees: ''
---
What's the question you have about lemmy?

3
.travis.yml vendored
View file

@ -24,10 +24,11 @@ script:
- cargo clippy -- -D clippy::style -D clippy::correctness -D clippy::complexity -D clippy::perf - cargo clippy -- -D clippy::style -D clippy::correctness -D clippy::complexity -D clippy::perf
- cargo install diesel_cli --no-default-features --features postgres --force - cargo install diesel_cli --no-default-features --features postgres --force
- diesel migration run - diesel migration run
- cargo test - cargo test --workspace
env: env:
global: global:
- DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy - DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
- LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
- RUST_TEST_THREADS=1 - RUST_TEST_THREADS=1
addons: addons:

2
ansible/VERSION vendored
View file

@ -1 +1 @@
v0.7.8 v0.7.16

12
ansible/lemmy.yml vendored
View file

@ -11,6 +11,7 @@
when: lemmy_base_dir is not defined when: lemmy_base_dir is not defined
- name: install python for Ansible - name: install python for Ansible
# python2-minimal instead of python-minimal for ubuntu 20.04 and up
raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-setuptools) raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-setuptools)
args: args:
executable: /bin/bash executable: /bin/bash
@ -27,7 +28,18 @@
- 'docker-compose' - 'docker-compose'
- 'docker.io' - 'docker.io'
- 'certbot' - 'certbot'
- name: install certbot-nginx on ubuntu < 20
apt:
pkg:
- 'python-certbot-nginx' - 'python-certbot-nginx'
when: ansible_distribution == 'Ubuntu' and ansible_distribution_version is version('20.04', '<')
- name: install certbot-nginx on ubuntu > 20
apt:
pkg:
- 'python3-certbot-nginx'
when: ansible_distribution == 'Ubuntu' and ansible_distribution_version is version('20.04', '>=')
- name: request initial letsencrypt certificate - name: request initial letsencrypt certificate
command: certbot certonly --nginx --agree-tos -d '{{ domain }}' -m '{{ letsencrypt_contact_email }}' command: certbot certonly --nginx --agree-tos -d '{{ domain }}' -m '{{ letsencrypt_contact_email }}'

View file

@ -35,7 +35,7 @@ services:
restart: always restart: always
iframely: iframely:
image: jolt/iframely:v1.4.3 image: dogbin/iframely:latest
ports: ports:
- "127.0.0.1:8061:80" - "127.0.0.1:8061:80"
volumes: volumes:

View file

@ -17,13 +17,20 @@ WORKDIR /app
RUN sudo chown -R rust:rust . RUN sudo chown -R rust:rust .
RUN USER=root cargo new server RUN USER=root cargo new server
WORKDIR /app/server WORKDIR /app/server
RUN mkdir -p lemmy_db/src/ lemmy_utils/src/
COPY server/Cargo.toml server/Cargo.lock ./ COPY server/Cargo.toml server/Cargo.lock ./
COPY server/lemmy_db/Cargo.toml ./lemmy_db/
COPY server/lemmy_utils/Cargo.toml ./lemmy_utils/
RUN sudo chown -R rust:rust . RUN sudo chown -R rust:rust .
RUN mkdir -p ./src/bin \ RUN mkdir -p ./src/bin \
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs && echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs \
&& cp ./src/bin/main.rs ./lemmy_db/src/main.rs \
&& cp ./src/bin/main.rs ./lemmy_utils/src/main.rs
RUN cargo build RUN cargo build
RUN rm -f ./target/x86_64-unknown-linux-musl/release/deps/lemmy_server* RUN rm -f ./target/x86_64-unknown-linux-musl/release/deps/lemmy_server*
COPY server/src ./src/ COPY server/src ./src/
COPY server/lemmy_db ./lemmy_db/
COPY server/lemmy_utils ./lemmy_utils/
COPY server/migrations ./migrations/ COPY server/migrations ./migrations/
# Build for debug # Build for debug

View file

@ -40,7 +40,7 @@ services:
restart: always restart: always
iframely: iframely:
image: jolt/iframely:v1.4.3 image: dogbin/iframely:latest
ports: ports:
- "127.0.0.1:8061:80" - "127.0.0.1:8061:80"
volumes: volumes:

View file

@ -1,6 +1,10 @@
#!/bin/bash #!/bin/bash
set -e set -e
# make sure there are no old containers or old data around
sudo docker-compose --file ../federation/docker-compose.yml --project-directory . down
sudo rm -rf volumes
pushd ../../server/ pushd ../../server/
cargo build cargo build
popd popd

View file

@ -107,6 +107,7 @@ services:
- ./volumes/postgres_gamma:/var/lib/postgresql/data - ./volumes/postgres_gamma:/var/lib/postgresql/data
iframely: iframely:
image: jolt/iframely:v1.4.3 image: dogbin/iframely:latest
volumes: volumes:
- ../iframely.config.local.js:/iframely/config.local.js:ro - ../iframely.config.local.js:/iframely/config.local.js:ro
restart: always

View file

@ -10,13 +10,19 @@ WORKDIR /app
RUN sudo chown -R rust:rust . RUN sudo chown -R rust:rust .
RUN USER=root cargo new server RUN USER=root cargo new server
WORKDIR /app/server WORKDIR /app/server
RUN mkdir -p lemmy_db/src/ lemmy_utils/src/
COPY --chown=rust:rust server/Cargo.toml server/Cargo.lock ./ COPY --chown=rust:rust server/Cargo.toml server/Cargo.lock ./
#RUN sudo chown -R rust:rust . COPY --chown=rust:rust server/lemmy_db/Cargo.toml ./lemmy_db/
COPY --chown=rust:rust server/lemmy_utils/Cargo.toml ./lemmy_utils/
RUN mkdir -p ./src/bin \ RUN mkdir -p ./src/bin \
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs && echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs \
&& cp ./src/bin/main.rs ./lemmy_db/src/main.rs \
&& cp ./src/bin/main.rs ./lemmy_utils/src/main.rs
RUN cargo build --release RUN cargo build --release
RUN rm -f ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/deps/lemmy_server* RUN rm -f ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/deps/lemmy_server*
COPY --chown=rust:rust server/src ./src/ COPY --chown=rust:rust server/src ./src/
COPY --chown=rust:rust server/lemmy_db ./lemmy_db/
COPY --chown=rust:rust server/lemmy_utils ./lemmy_utils/
COPY --chown=rust:rust server/migrations ./migrations/ COPY --chown=rust:rust server/migrations ./migrations/
# build for release # build for release

View file

@ -12,7 +12,7 @@ services:
restart: always restart: always
lemmy: lemmy:
image: dessalines/lemmy:v0.7.8 image: dessalines/lemmy:v0.7.16
ports: ports:
- "127.0.0.1:8536:8536" - "127.0.0.1:8536:8536"
restart: always restart: always
@ -35,7 +35,7 @@ services:
restart: always restart: always
iframely: iframely:
image: jolt/iframely:v1.4.3 image: dogbin/iframely:latest
ports: ports:
- "127.0.0.1:8061:80" - "127.0.0.1:8061:80"
volumes: volumes:

View file

@ -1,16 +1,21 @@
#!/bin/bash #!/bin/bash
set -e set -e
if [[ $(id -u) != 0 ]]; then if [[ $(id -u) != 0 ]]; then
echo "This migration needs to be run as root" echo "This migration needs to be run as root"
exit exit
fi fi
if [[ ! -f docker-compose.yml ]]; then if [[ ! -f docker-compose.yml ]]; then
echo "No docker-compose.yml found in current directory. Is this the right folder?" echo "No docker-compose.yml found in current directory. Is this the right folder?"
exit exit
fi fi
if ! which jq > /dev/null; then
echo "jq must be installed to run this migration. On ubuntu systems, try 'sudo apt-get install jq'"
exit
fi
# Fixing pictrs permissions # Fixing pictrs permissions
mkdir -p volumes/pictrs mkdir -p volumes/pictrs
sudo chown -R 991:991 volumes/pictrs sudo chown -R 991:991 volumes/pictrs
@ -26,6 +31,8 @@ fi
# echo "Stopping Lemmy so that users dont upload new images during the migration" # echo "Stopping Lemmy so that users dont upload new images during the migration"
# docker-compose stop lemmy # docker-compose stop lemmy
CRASHED_ON=()
pushd volumes/pictshare/ pushd volumes/pictshare/
echo "Importing pictshare images to pict-rs..." echo "Importing pictshare images to pict-rs..."
IMAGE_NAMES=* IMAGE_NAMES=*
@ -34,11 +41,36 @@ for image in $IMAGE_NAMES; do
if [[ ! -f $IMAGE_PATH ]]; then if [[ ! -f $IMAGE_PATH ]]; then
continue continue
fi fi
echo -e "\nImporting $IMAGE_PATH" res=$(curl -s -F "images[]=@$IMAGE_PATH" http://127.0.0.1:8537/import | jq .msg)
ret=0 if [ "${res}" == "" ]; then
curl --silent --fail -F "images[]=@$IMAGE_PATH" http://127.0.0.1:8537/import || ret=$? echo -n "C" >&2
if [[ $ret != 0 ]]; then echo ""
echo "Error for $IMAGE_PATH : $ret" CRASHED_ON+=("${IMAGE_PATH}")
echo "Failed to import $IMAGE_PATH with no error message"
echo " assuming crash, sleeping"
sleep 10
continue
fi
if [ "${res}" != "\"ok\"" ]; then
echo -n "F" >&2
echo ""
echo "Failed to import $IMAGE_PATH"
echo " Reason: ${res}"
else
echo -n "." >&2
fi
done
for image in ${CRASHED_ON[@]}; do
echo "Retrying ${image}"
res=$(curl -s -F "images[]=@$IMAGE_PATH" http://127.0.0.1:8537/import | jq .msg)
if [ "${res}" != "\"ok\"" ]; then
echo -n "F" >&2
echo ""
echo "Failed to upload ${image} on 2nd attempt"
echo " Reason: ${res}"
else
echo -n "." >&2
fi fi
done done

View file

@ -7,7 +7,7 @@ can copy the options you want to change into your local `config.hjson` file.
Additionally, you can override any config files with environment variables. These have the same Additionally, you can override any config files with environment variables. These have the same
name as the config options, and are prefixed with `LEMMY_`. For example, you can override the name as the config options, and are prefixed with `LEMMY_`. For example, you can override the
`database.password` with `LEMMY__DATABASE__POOL_SIZE=10`. `database.password` with `LEMMY_DATABASE__POOL_SIZE=10`.
An additional option `LEMMY_DATABASE_URL` is available, which can be used with a PostgreSQL An additional option `LEMMY_DATABASE_URL` is available, which can be used with a PostgreSQL
connection string like `postgres://lemmy:password@lemmy_db:5432/lemmy`, passing all connection connection string like `postgres://lemmy:password@lemmy_db:5432/lemmy`, passing all connection

View file

@ -5,14 +5,7 @@
If you don't have a local clone of the Lemmy repo yet, just run the following command: If you don't have a local clone of the Lemmy repo yet, just run the following command:
```bash ```bash
git clone https://github.com/LemmyNet/lemmy -b federation git clone https://github.com/LemmyNet/lemmy
```
If you already have the Lemmy repo cloned, you need to add a new remote:
```bash
git remote add federation https://github.com/LemmyNet/lemmy
git checkout federation
git pull federation federation
``` ```
## Running locally ## Running locally
@ -26,18 +19,34 @@ You need to have the following packages installed, the Docker service needs to b
Then run the following Then run the following
```bash ```bash
cd dev/federation-test cd docker/federation
./run-federation-test.bash ./run-federation-test.bash -yarn
``` ```
After the build is finished and the docker-compose setup is running, open [127.0.0.1:8540](http://127.0.0.1:8540) and The federation test sets up 3 instances:
[127.0.0.1:8550](http://127.0.0.1:8550) in your browser to use the test instances. You can login as admin with
username `lemmy_alpha` and `lemmy_beta` respectively, with password `lemmy`. Instance / Username | Location
--- | ---
lemmy_alpha | [127.0.0.1:8540](http://127.0.0.1:8540)
lemmy_beta | [127.0.0.1:8550](http://127.0.0.1:8550)
lemmy_gamma | [127.0.0.1:8560](http://127.0.0.1:8560)
You can log into each using the instance name, and `lemmy` as the password, IE (`lemmy_alpha`, `lemmy`).
Firefox containers are a good way to test them interacting.
## Integration tests
To run a suite of suite of federation integration tests:
```bash
cd docker/federation-test
./run-tests.sh
```
## Running on a server ## Running on a server
Note that federation is currently in alpha. Only use it for testing, not on any production server, and be aware Note that federation is currently in alpha. **Only use it for testing**, not on any production server, and be aware that turning on federation may break your instance.
that you might have to wipe the instance data at one point or another.
Follow the normal installation instructions, either with [Ansible](administration_install_ansible.md) or Follow the normal installation instructions, either with [Ansible](administration_install_ansible.md) or
[manually](administration_install_docker.md). Then replace the line `image: dessalines/lemmy:v0.x.x` in [manually](administration_install_docker.md). Then replace the line `image: dessalines/lemmy:v0.x.x` in
@ -47,11 +56,12 @@ Follow the normal installation instructions, either with [Ansible](administratio
``` ```
federation: { federation: {
enabled: true enabled: true
allowed_instances: example.com tls_enabled: true,
allowed_instances: example.com,
} }
``` ```
Afterwards, and whenver you want to update to the latest version, run these commands on the server: Afterwards, and whenever you want to update to the latest version, run these commands on the server:
``` ```
cd /lemmy/ cd /lemmy/

View file

@ -7,9 +7,7 @@ following commands in the `server` subfolder:
```bash ```bash
psql -U lemmy -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" psql -U lemmy -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy ./test.sh
diesel migration run
RUST_TEST_THREADS=1 cargo test
``` ```
### Federation ### Federation

View file

@ -3,30 +3,52 @@
- A group of lemmy developers and users that use a well-defined democratic process to steer the project in a positive direction, keep it aligned to community goals, and resolve conflicts. - A group of lemmy developers and users that use a well-defined democratic process to steer the project in a positive direction, keep it aligned to community goals, and resolve conflicts.
- Council members are also added as administrators to any official Lemmy instances. - Council members are also added as administrators to any official Lemmy instances.
## Voting / Decision-Making ## 1. What gets voted on
### Process This section describes all the aspects of Lemmy where the council has decision making power, namely:
- Anything is open for discussion
- Voting done through matrix chat reacts (thumbs up/thumbs down)
- Require a simple majority for votes. (Maybe 2/3rds for more debated decisions).
- Once a decision is reached democratically, the dicision is binding and all group members have to follow it
- All members of the Lemmy council have equal voting power.
- Voting must stay open for at least 2 days.
### What gets voted on
- Membership (joining, removing)
- Coding direction - Coding direction
- Priorities / Emphasis - Priorities / Emphasis
- Controversial features (For example, an unpopular feature should be removed) - Controversial features (For example, an unpopular feature should be removed)
- Communication mediums - Moderation and conflict resolution on:
- Conflict resolution - [dev.lemmy.ml](https://dev.lemmy.ml/)
- dev.lemmy.ml (domain and server) - [github.com/LemmyNet/lemmy](https://github.com/LemmyNet/lemmy)
- lemmy.ml and subdomains (excluding communism.lemmy.ml) - [yerbamate.dev/LemmyNet/lemmy](https://yerbamate.dev/LemmyNet/lemmy)
- git repo including mirrors (on github, gitea, etc) - [weblate.yerbamate.dev/projects/lemmy/](https://weblate.yerbamate.dev/projects/lemmy/)
- Any official accounts of the Lemmy project, for example the Mastodon account or the Liberapay account - Technical administration of dev.lemmy.ml
- Official Lemmy accounts
- [Mastodon](https://mastodon.social/@LemmyDev)
- [Liberapay](https://liberapay.com/Lemmy/)
- [Patreon](https://www.patreon.com/dessalines)
- Council membership changes
- Changes to these rules - Changes to these rules
## Joining ## 2. Feedback and Activity Reports
Every week, the council should make a thread on Lemmy that details its activity during the past week, be it development, moderation, or anything else mentioned in 1.
At the same time, users can give feedback and suggestions in this thread. This should be taken into account by the council. Council members can call for a vote on any controversial issues, if they can't be resolved by discussion.
## 2. Voting Process
Most of the time, we keep each other up to date through the Matrix chat, and take informal decisions on uncontroversial issues. For example, a user clearly violating the site rules could be banned by a single person, or ideally after discussing it with at least one other member.
If an issue can not be resolved in this way, then any council member can call for a vote, which works in the following way:
- Any council member can call for a vote, on any topic mentioned in 1.
- This should be used if there is any controversy in the community, or between council members.
- Before taking any decision, there needs to be a discussion where every council member can
explain their position.
- Discussion should be taken with the goal of reaching a compromise that is acceptable for
everyone.
- After the discussion, voting is done through Matrix emojis (👍: yes, 👎: no, X: abstain) and must
stay open for at least two days.
- All members of the Lemmy council have equal voting power.
- Decisions should be reached unanimously, or nearly so. If this is not possible, at least
2/3 of votes must be in favour for the motion to pass.
- Once a decision is reached in this way, every member needs to abide by it.
## 4. Joining
- We use the following process: anyone who is active around Lemmy can recommend any other active person to join the council. This has to be approved by a majority of the council. - We use the following process: anyone who is active around Lemmy can recommend any other active person to join the council. This has to be approved by a majority of the council.
- Active users are defined as those who contribute to Lemmy in some way for at least an hour per week on average, doing things like reporting bugs, discussing rules and features, translating, promoting, developing, or doing other things that aim to improve Lemmy as a whole. - Active users are defined as those who contribute to Lemmy in some way for at least an hour per week on average, doing things like reporting bugs, discussing rules and features, translating, promoting, developing, or doing other things that aim to improve Lemmy as a whole.
-> people should have joined at least a month ago. -> people should have joined at least a month ago.
@ -34,23 +56,24 @@
- Note: we would like to have a process where community members can elect candidates for the council, but this is not realistic because a single user could easily create multiple accounts and cheat the vote. - Note: we would like to have a process where community members can elect candidates for the council, but this is not realistic because a single user could easily create multiple accounts and cheat the vote.
- Limit growth to one new member per month at most. - Limit growth to one new member per month at most.
## Removing members ## 5. Removing members
- Inactive members should be removed from the council after a few months of inactivity, and after receiving a notification about this. - Inactive members should be removed from the council after a few months of inactivity, and after receiving a notification about this.
- Members that dont follow binding council decisions should be removed. - Members that dont follow binding council decisions should be removed.
- Any member can be removed in a vote. - Any member can be removed in a vote.
## Goals ## 6. Goals
- We encourage the membership of groups such as LGBT, religious or ethnic minorities, abuse victims, etc etc, and strive to create a safe space for them to express their opinions. We also support measures to increase participation by the previously mentioned groups. - We encourage the membership of groups such as LGBT, religious or ethnic minorities, abuse victims, etc etc, and strive to create a safe space for them to express their opinions. We also support measures to increase participation by the previously mentioned groups.
- The following are banned, and will always be harshly punished: fascism, abuse, racism, sexism, etc etc, - The following are banned, and will always be harshly punished: fascism, abuse, racism, sexism, etc etc,
## Communication ## 7. Communication
- A private Matrix chat for all council members. - A private Matrix chat for all council members.
- (Once private communities are done) A private community on dev.lemmy.ml for issues. - (Once private communities are done) A private community on dev.lemmy.ml for issues.
## Member List / Contact Info ## 8. Member List / Contact Info
General Contact [@LemmyDev Mastodon](https://mastodon.social/@LemmyDev) General Contact [@LemmyDev Mastodon](https://mastodon.social/@LemmyDev)
- [Dessalines](https://dev.lemmy.ml/u/dessalines) - [Dessalines](https://dev.lemmy.ml/u/dessalines)
- [Nutomic](https://dev.lemmy.ml/u/nutomic) - [Nutomic](https://dev.lemmy.ml/u/nutomic)
- [AgreeableLandscape](https://dev.lemmy.ml/u/AgreeableLandscape) - [AgreeableLandscape](https://dev.lemmy.ml/u/AgreeableLandscape)
- [fruechtchen](https://dev.lemmy.ml/u/fruechtchen) - [fruechtchen](https://dev.lemmy.ml/u/fruechtchen)
- [kixiQu](https://dev.lemmy.ml/u/kixiQu)

2
install.sh vendored
View file

@ -1,4 +1,4 @@
#!/bin/sh #!/bin/bash
set -e set -e
# Set the database variable to the default first. # Set the database variable to the default first.

194
server/Cargo.lock generated vendored
View file

@ -63,7 +63,7 @@ dependencies = [
"futures-util", "futures-util",
"log", "log",
"once_cell", "once_cell",
"parking_lot", "parking_lot 0.10.2",
"pin-project", "pin-project",
"smallvec", "smallvec",
"tokio", "tokio",
@ -269,7 +269,7 @@ dependencies = [
"lazy_static", "lazy_static",
"log", "log",
"num_cpus", "num_cpus",
"parking_lot", "parking_lot 0.10.2",
"threadpool", "threadpool",
] ]
@ -397,6 +397,12 @@ dependencies = [
"gimli", "gimli",
] ]
[[package]]
name = "adler"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccc9a9dd069569f212bc4330af9f17c4afb5e8ce185e83dbb14f1349dda18b10"
[[package]] [[package]]
name = "adler32" name = "adler32"
version = "1.1.0" version = "1.1.0"
@ -506,7 +512,7 @@ dependencies = [
"addr2line", "addr2line",
"cfg-if", "cfg-if",
"libc", "libc",
"miniz_oxide", "miniz_oxide 0.3.7",
"object", "object",
"rustc-demangle", "rustc-demangle",
] ]
@ -680,9 +686,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.55" version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1be3409f94d7bdceeb5f5fac551039d9b3f00e25da7a74fc4d33400a0d96368" checksum = "0fde55d2a2bfaa4c9668bbc63f531fbdeee3ffe188f4662511ce2c22b3eedebe"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -692,9 +698,9 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.11" version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" checksum = "f0fee792e164f78f5fe0c296cc2eb3688a2ca2b70cdff33040922d298203f0c4"
dependencies = [ dependencies = [
"num-integer", "num-integer",
"num-traits 0.2.12", "num-traits 0.2.12",
@ -726,6 +732,15 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "cloudabi"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "comrak" name = "comrak"
version = "0.7.0" version = "0.7.0"
@ -1120,14 +1135,14 @@ dependencies = [
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.14" version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" checksum = "68c90b0fc46cf89d227cc78b40e494ff81287a92dd07631e5af0d06fe3cf885e"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"crc32fast", "crc32fast",
"libc", "libc",
"miniz_oxide", "miniz_oxide 0.4.0",
] ]
[[package]] [[package]]
@ -1384,12 +1399,6 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "htmlescape"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163"
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.1" version = "0.2.1"
@ -1470,6 +1479,12 @@ dependencies = [
"autocfg 1.0.0", "autocfg 1.0.0",
] ]
[[package]]
name = "instant"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69da7ce1490173c2bf4d26bc8be429aaeeaf4cce6c4b970b7949651fa17655fe"
[[package]] [[package]]
name = "iovec" name = "iovec"
version = "0.1.4" version = "0.1.4"
@ -1508,18 +1523,18 @@ checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.40" version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce10c23ad2ea25ceca0093bd3192229da4c5b3c0f2de499c1ecac0d98d452177" checksum = "c4b9172132a62451e56142bff9afc91c8e4a4500aa5b847da36815b63bfda916"
dependencies = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]] [[package]]
name = "jsonwebtoken" name = "jsonwebtoken"
version = "7.1.2" version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f325ae57ddcf609f02d891486ce740f5bbd0cc3e93f9bffaacdf6594b21404" checksum = "afabcc15e437a6484fc4f12d0fd63068fe457bf93f1c148d3d9649c60b103f32"
dependencies = [ dependencies = [
"base64 0.12.3", "base64 0.12.3",
"pem", "pem",
@ -1551,6 +1566,21 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lemmy_db"
version = "0.1.0"
dependencies = [
"bcrypt",
"chrono",
"diesel",
"log",
"serde 1.0.114",
"serde_json",
"sha2",
"strum",
"strum_macros",
]
[[package]] [[package]]
name = "lemmy_server" name = "lemmy_server"
version = "0.0.1" version = "0.0.1"
@ -1568,27 +1598,23 @@ dependencies = [
"base64 0.12.3", "base64 0.12.3",
"bcrypt", "bcrypt",
"chrono", "chrono",
"comrak",
"config",
"diesel", "diesel",
"diesel_migrations", "diesel_migrations",
"dotenv", "dotenv",
"env_logger", "env_logger",
"failure", "failure",
"futures", "futures",
"htmlescape",
"http", "http",
"http-signature-normalization-actix", "http-signature-normalization-actix",
"itertools", "itertools",
"jsonwebtoken", "jsonwebtoken",
"lazy_static", "lazy_static",
"lettre", "lemmy_db",
"lettre_email", "lemmy_utils",
"log", "log",
"openssl", "openssl",
"percent-encoding", "percent-encoding",
"rand 0.7.3", "rand 0.7.3",
"regex",
"rss", "rss",
"serde 1.0.114", "serde 1.0.114",
"serde_json", "serde_json",
@ -1600,6 +1626,26 @@ dependencies = [
"uuid 0.8.1", "uuid 0.8.1",
] ]
[[package]]
name = "lemmy_utils"
version = "0.1.0"
dependencies = [
"chrono",
"comrak",
"config",
"itertools",
"lazy_static",
"lettre",
"lettre_email",
"log",
"openssl",
"rand 0.7.3",
"regex",
"serde 1.0.114",
"serde_json",
"url",
]
[[package]] [[package]]
name = "lettre" name = "lettre"
version = "0.9.3" version = "0.9.3"
@ -1676,6 +1722,15 @@ dependencies = [
"scopeguard", "scopeguard",
] ]
[[package]]
name = "lock_api"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de302ce1fe7482db13738fbaf2e21cfb06a986b89c0bf38d88abf16681aada4e"
dependencies = [
"scopeguard",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.8" version = "0.4.8"
@ -1781,6 +1836,15 @@ dependencies = [
"adler32", "adler32",
] ]
[[package]]
name = "miniz_oxide"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be0f75932c1f6cfae3c04000e40114adf955636e19040f9c0a2c380702aa1c7f"
dependencies = [
"adler",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.6.22" version = "0.6.22"
@ -1985,8 +2049,19 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e"
dependencies = [ dependencies = [
"lock_api", "lock_api 0.3.4",
"parking_lot_core", "parking_lot_core 0.7.2",
]
[[package]]
name = "parking_lot"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733"
dependencies = [
"instant",
"lock_api 0.4.0",
"parking_lot_core 0.8.0",
] ]
[[package]] [[package]]
@ -1996,7 +2071,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cloudabi", "cloudabi 0.0.3",
"libc",
"redox_syscall",
"smallvec",
"winapi 0.3.9",
]
[[package]]
name = "parking_lot_core"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b"
dependencies = [
"cfg-if",
"cloudabi 0.1.0",
"instant",
"libc", "libc",
"redox_syscall", "redox_syscall",
"smallvec", "smallvec",
@ -2164,12 +2254,12 @@ dependencies = [
[[package]] [[package]]
name = "r2d2" name = "r2d2"
version = "0.8.8" version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1497e40855348e4a8a40767d8e55174bce1e445a3ac9254ad44ad468ee0485af" checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f"
dependencies = [ dependencies = [
"log", "log",
"parking_lot", "parking_lot 0.11.0",
"scheduled-thread-pool", "scheduled-thread-pool",
] ]
@ -2306,7 +2396,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
dependencies = [ dependencies = [
"cloudabi", "cloudabi 0.0.3",
"fuchsia-cprng", "fuchsia-cprng",
"libc", "libc",
"rand_core 0.4.2", "rand_core 0.4.2",
@ -2462,11 +2552,11 @@ dependencies = [
[[package]] [[package]]
name = "scheduled-thread-pool" name = "scheduled-thread-pool"
version = "0.2.4" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0988d7fdf88d5e5fcf5923a0f1e8ab345f3e98ab4bc6bc45a2d5ff7f7458fbf6" checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7"
dependencies = [ dependencies = [
"parking_lot", "parking_lot 0.11.0",
] ]
[[package]] [[package]]
@ -2570,9 +2660,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.55" version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec2c5d7e739bc07a3e73381a39d61fdb5f671c60c1df26a130690665803d8226" checksum = "3433e879a558dde8b5e8feb2a04899cf34fdde1fafb894687e52105fc1162ac3"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"itoa", "itoa",
@ -3106,9 +3196,9 @@ checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.7" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
@ -3222,9 +3312,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.63" version = "0.2.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c2dc4aa152834bc334f506c1a06b866416a8b6697d5c9f75b9a689c8486def0" checksum = "6a634620115e4a229108b71bde263bb4220c483b3f07f5ba514ee8d15064c4c2"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"wasm-bindgen-macro", "wasm-bindgen-macro",
@ -3232,9 +3322,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.63" version = "0.2.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ded84f06e0ed21499f6184df0e0cb3494727b0c5da89534e0fcc55c51d812101" checksum = "3e53963b583d18a5aa3aaae4b4c1cb535218246131ba22a71f05b518098571df"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"lazy_static", "lazy_static",
@ -3247,9 +3337,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.63" version = "0.2.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "838e423688dac18d73e31edce74ddfac468e37b1506ad163ffaf0a46f703ffe3" checksum = "3fcfd5ef6eec85623b4c6e844293d4516470d8f19cd72d0d12246017eb9060b8"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -3257,9 +3347,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.63" version = "0.2.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3156052d8ec77142051a533cdd686cba889537b213f948cd1d20869926e68e92" checksum = "9adff9ee0e94b926ca81b57f57f86d5545cdcb1d259e21ec9bdd95b901754c75"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3270,15 +3360,15 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.63" version = "0.2.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9ba19973a58daf4db6f352eda73dc0e289493cd29fb2632eb172085b6521acd" checksum = "7f7b90ea6c632dd06fd765d44542e234d5e63d9bb917ecd64d79778a13bd79ae"
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.40" version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b72fe77fd39e4bd3eaa4412fd299a0be6b3dfe9d2597e2f1c20beb968f41d17" checksum = "863539788676619aac1a23e2df3655e96b32b0e05eb72ca34ba045ad573c625d"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",

17
server/Cargo.toml vendored
View file

@ -1,14 +1,21 @@
[package] [package]
name = "lemmy_server" name = "lemmy_server"
version = "0.0.1" version = "0.0.1"
authors = ["Dessalines <tyhou13@gmx.com>"]
edition = "2018" edition = "2018"
[profile.release] [profile.release]
lto = true lto = true
[workspace]
members = [
"lemmy_utils",
"lemmy_db"
]
[dependencies] [dependencies]
diesel = { version = "1.4.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json"] } lemmy_utils = { path = "./lemmy_utils" }
lemmy_db = { path = "./lemmy_db" }
diesel = "1.4.4"
diesel_migrations = "1.4.0" diesel_migrations = "1.4.0"
dotenv = "0.15.0" dotenv = "0.15.0"
activitystreams = "0.6.2" activitystreams = "0.6.2"
@ -31,16 +38,10 @@ rand = "0.7.3"
strum = "0.18.0" strum = "0.18.0"
strum_macros = "0.18.0" strum_macros = "0.18.0"
jsonwebtoken = "7.0.1" jsonwebtoken = "7.0.1"
regex = "1.3.5"
lazy_static = "1.3.0" lazy_static = "1.3.0"
lettre = "0.9.3"
lettre_email = "0.9.4"
rss = "1.9.0" rss = "1.9.0"
htmlescape = "0.3.1"
url = { version = "2.1.1", features = ["serde"] } url = { version = "2.1.1", features = ["serde"] }
config = {version = "0.10.1", default-features = false, features = ["hjson"] }
percent-encoding = "2.1.0" percent-encoding = "2.1.0"
comrak = "0.7"
openssl = "0.10" openssl = "0.10"
http = "0.2.1" http = "0.2.1"
http-signature-normalization-actix = { version = "0.4.0-alpha.0", default-features = false, features = ["sha-2"] } http-signature-normalization-actix = { version = "0.4.0-alpha.0", default-features = false, features = ["sha-2"] }

3
server/db-init.sh vendored
View file

@ -1,4 +1,5 @@
#!/bin/sh #!/bin/bash
set -e
# Default configurations # Default configurations
username=lemmy username=lemmy

2
server/diesel.toml vendored
View file

@ -2,4 +2,4 @@
# see diesel.rs/guides/configuring-diesel-cli # see diesel.rs/guides/configuring-diesel-cli
[print_schema] [print_schema]
file = "src/schema.rs" file = "lemmy_db/src/schema.rs"

15
server/lemmy_db/Cargo.toml vendored Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "lemmy_db"
version = "0.1.0"
edition = "2018"
[dependencies]
diesel = { version = "1.4.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json"] }
chrono = { version = "0.4.7", features = ["serde"] }
serde = { version = "1.0.105", features = ["derive"] }
serde_json = { version = "1.0.52", features = ["preserve_order"]}
strum = "0.18.0"
strum_macros = "0.18.0"
log = "0.4.0"
sha2 = "0.9"
bcrypt = "0.8.0"

View file

@ -1,9 +1,12 @@
use crate::{blocking, db::Crud, schema::activity, DbPool, LemmyError}; use crate::{schema::activity, Crud};
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use log::debug; use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::fmt::Debug; use std::{
fmt::Debug,
io::{Error as IoError, ErrorKind},
};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "activity"] #[table_name = "activity"]
@ -55,46 +58,43 @@ impl Crud<ActivityForm> for Activity {
} }
} }
pub async fn insert_activity<T>( pub fn do_insert_activity<T>(
user_id: i32,
data: T,
local: bool,
pool: &DbPool,
) -> Result<(), LemmyError>
where
T: Serialize + Debug + Send + 'static,
{
blocking(pool, move |conn| {
do_insert_activity(conn, user_id, &data, local)
})
.await??;
Ok(())
}
fn do_insert_activity<T>(
conn: &PgConnection, conn: &PgConnection,
user_id: i32, user_id: i32,
data: &T, data: &T,
local: bool, local: bool,
) -> Result<(), LemmyError> ) -> Result<Activity, IoError>
where where
T: Serialize + Debug, T: Serialize + Debug,
{ {
debug!("inserting activity for user {}, data {:?}", user_id, &data);
let activity_form = ActivityForm { let activity_form = ActivityForm {
user_id, user_id,
data: serde_json::to_value(&data)?, data: serde_json::to_value(&data)?,
local, local,
updated: None, updated: None,
}; };
debug!("inserting activity for user {}, data {:?}", user_id, data); let result = Activity::create(&conn, &activity_form);
Activity::create(&conn, &activity_form)?; match result {
Ok(()) Ok(s) => Ok(s),
Err(e) => Err(IoError::new(
ErrorKind::Other,
format!("Failed to insert activity into database: {}", e),
)),
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{super::user::*, *}; use crate::{
use crate::db::{establish_unpooled_connection, Crud, ListingType, SortType}; activity::{Activity, ActivityForm},
tests::establish_unpooled_connection,
user::{UserForm, User_},
Crud,
ListingType,
SortType,
};
use serde_json::Value;
#[test] #[test]
fn test_crud() { fn test_crud() {

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
db::Crud,
schema::{category, category::dsl::*}, schema::{category, category::dsl::*},
Crud,
}; };
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -52,8 +52,7 @@ impl Category {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use crate::{category::Category, tests::establish_unpooled_connection};
use crate::db::establish_unpooled_connection;
#[test] #[test]
fn test_crud() { fn test_crud() {

View file

@ -1,9 +1,5 @@
use super::{post::Post, *}; use super::{post::Post, *};
use crate::{ use crate::schema::{comment, comment_like, comment_saved};
apub::{make_apub_endpoint, EndpointType},
naive_now,
schema::{comment, comment_like, comment_saved},
};
// WITH RECURSIVE MyTree AS ( // WITH RECURSIVE MyTree AS (
// SELECT * FROM comment WHERE parent_id IS NULL // SELECT * FROM comment WHERE parent_id IS NULL
@ -77,12 +73,15 @@ impl Crud<CommentForm> for Comment {
} }
impl Comment { impl Comment {
pub fn update_ap_id(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> { pub fn update_ap_id(
conn: &PgConnection,
comment_id: i32,
apub_id: String,
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*; use crate::schema::comment::dsl::*;
let apid = make_apub_endpoint(EndpointType::Comment, &comment_id.to_string()).to_string();
diesel::update(comment.find(comment_id)) diesel::update(comment.find(comment_id))
.set(ap_id.eq(apid)) .set(ap_id.eq(apub_id))
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
@ -204,10 +203,8 @@ impl Saveable<CommentSavedForm> for CommentSaved {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use crate::{comment::*, community::*, post::*, tests::establish_unpooled_connection, user::*};
super::{community::*, post::*, user::*},
*,
};
#[test] #[test]
fn test_crud() { fn test_crud() {
let conn = establish_unpooled_connection(); let conn = establish_unpooled_connection();

View file

@ -1,4 +1,5 @@
use crate::db::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType}; // TODO, remove the cross join here, just join to user directly
use crate::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType};
use diesel::{dsl::*, pg::Pg, result::Error, *}; use diesel::{dsl::*, pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -26,6 +27,7 @@ table! {
creator_actor_id -> Text, creator_actor_id -> Text,
creator_local -> Bool, creator_local -> Bool,
creator_name -> Varchar, creator_name -> Varchar,
creator_published -> Timestamp,
creator_avatar -> Nullable<Text>, creator_avatar -> Nullable<Text>,
score -> BigInt, score -> BigInt,
upvotes -> BigInt, upvotes -> BigInt,
@ -39,7 +41,7 @@ table! {
} }
table! { table! {
comment_mview (id) { comment_fast_view (id) {
id -> Int4, id -> Int4,
creator_id -> Int4, creator_id -> Int4,
post_id -> Int4, post_id -> Int4,
@ -61,6 +63,7 @@ table! {
creator_actor_id -> Text, creator_actor_id -> Text,
creator_local -> Bool, creator_local -> Bool,
creator_name -> Varchar, creator_name -> Varchar,
creator_published -> Timestamp,
creator_avatar -> Nullable<Text>, creator_avatar -> Nullable<Text>,
score -> BigInt, score -> BigInt,
upvotes -> BigInt, upvotes -> BigInt,
@ -76,7 +79,7 @@ table! {
#[derive( #[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)] )]
#[table_name = "comment_view"] #[table_name = "comment_fast_view"]
pub struct CommentView { pub struct CommentView {
pub id: i32, pub id: i32,
pub creator_id: i32, pub creator_id: i32,
@ -99,6 +102,7 @@ pub struct CommentView {
pub creator_actor_id: String, pub creator_actor_id: String,
pub creator_local: bool, pub creator_local: bool,
pub creator_name: String, pub creator_name: String,
pub creator_published: chrono::NaiveDateTime,
pub creator_avatar: Option<String>, pub creator_avatar: Option<String>,
pub score: i64, pub score: i64,
pub upvotes: i64, pub upvotes: i64,
@ -112,7 +116,7 @@ pub struct CommentView {
pub struct CommentQueryBuilder<'a> { pub struct CommentQueryBuilder<'a> {
conn: &'a PgConnection, conn: &'a PgConnection,
query: super::comment_view::comment_mview::BoxedQuery<'a, Pg>, query: super::comment_view::comment_fast_view::BoxedQuery<'a, Pg>,
listing_type: ListingType, listing_type: ListingType,
sort: &'a SortType, sort: &'a SortType,
for_community_id: Option<i32>, for_community_id: Option<i32>,
@ -127,9 +131,9 @@ pub struct CommentQueryBuilder<'a> {
impl<'a> CommentQueryBuilder<'a> { impl<'a> CommentQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection) -> Self { pub fn create(conn: &'a PgConnection) -> Self {
use super::comment_view::comment_mview::dsl::*; use super::comment_view::comment_fast_view::dsl::*;
let query = comment_mview.into_boxed(); let query = comment_fast_view.into_boxed();
CommentQueryBuilder { CommentQueryBuilder {
conn, conn,
@ -198,7 +202,7 @@ impl<'a> CommentQueryBuilder<'a> {
} }
pub fn list(self) -> Result<Vec<CommentView>, Error> { pub fn list(self) -> Result<Vec<CommentView>, Error> {
use super::comment_view::comment_mview::dsl::*; use super::comment_view::comment_fast_view::dsl::*;
let mut query = self.query; let mut query = self.query;
@ -270,8 +274,8 @@ impl CommentView {
from_comment_id: i32, from_comment_id: i32,
my_user_id: Option<i32>, my_user_id: Option<i32>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
use super::comment_view::comment_mview::dsl::*; use super::comment_view::comment_fast_view::dsl::*;
let mut query = comment_mview.into_boxed(); let mut query = comment_fast_view.into_boxed();
// The view lets you pass a null user_id, if you're not logged in // The view lets you pass a null user_id, if you're not logged in
if let Some(my_user_id) = my_user_id { if let Some(my_user_id) = my_user_id {
@ -290,7 +294,7 @@ impl CommentView {
// The faked schema since diesel doesn't do views // The faked schema since diesel doesn't do views
table! { table! {
reply_view (id) { reply_fast_view (id) {
id -> Int4, id -> Int4,
creator_id -> Int4, creator_id -> Int4,
post_id -> Int4, post_id -> Int4,
@ -313,6 +317,7 @@ table! {
creator_local -> Bool, creator_local -> Bool,
creator_name -> Varchar, creator_name -> Varchar,
creator_avatar -> Nullable<Text>, creator_avatar -> Nullable<Text>,
creator_published -> Timestamp,
score -> BigInt, score -> BigInt,
upvotes -> BigInt, upvotes -> BigInt,
downvotes -> BigInt, downvotes -> BigInt,
@ -328,7 +333,7 @@ table! {
#[derive( #[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)] )]
#[table_name = "reply_view"] #[table_name = "reply_fast_view"]
pub struct ReplyView { pub struct ReplyView {
pub id: i32, pub id: i32,
pub creator_id: i32, pub creator_id: i32,
@ -352,6 +357,7 @@ pub struct ReplyView {
pub creator_local: bool, pub creator_local: bool,
pub creator_name: String, pub creator_name: String,
pub creator_avatar: Option<String>, pub creator_avatar: Option<String>,
pub creator_published: chrono::NaiveDateTime,
pub score: i64, pub score: i64,
pub upvotes: i64, pub upvotes: i64,
pub downvotes: i64, pub downvotes: i64,
@ -365,7 +371,7 @@ pub struct ReplyView {
pub struct ReplyQueryBuilder<'a> { pub struct ReplyQueryBuilder<'a> {
conn: &'a PgConnection, conn: &'a PgConnection,
query: super::comment_view::reply_view::BoxedQuery<'a, Pg>, query: super::comment_view::reply_fast_view::BoxedQuery<'a, Pg>,
for_user_id: i32, for_user_id: i32,
sort: &'a SortType, sort: &'a SortType,
unread_only: bool, unread_only: bool,
@ -375,9 +381,9 @@ pub struct ReplyQueryBuilder<'a> {
impl<'a> ReplyQueryBuilder<'a> { impl<'a> ReplyQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection, for_user_id: i32) -> Self { pub fn create(conn: &'a PgConnection, for_user_id: i32) -> Self {
use super::comment_view::reply_view::dsl::*; use super::comment_view::reply_fast_view::dsl::*;
let query = reply_view.into_boxed(); let query = reply_fast_view.into_boxed();
ReplyQueryBuilder { ReplyQueryBuilder {
conn, conn,
@ -411,7 +417,7 @@ impl<'a> ReplyQueryBuilder<'a> {
} }
pub fn list(self) -> Result<Vec<ReplyView>, Error> { pub fn list(self) -> Result<Vec<ReplyView>, Error> {
use super::comment_view::reply_view::dsl::*; use super::comment_view::reply_fast_view::dsl::*;
let mut query = self.query; let mut query = self.query;
@ -454,11 +460,17 @@ impl<'a> ReplyQueryBuilder<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use crate::{
super::{comment::*, community::*, post::*, user::*}, comment::*,
comment_view::*,
community::*,
post::*,
tests::establish_unpooled_connection,
user::*,
Crud,
Likeable,
*, *,
}; };
use crate::db::{establish_unpooled_connection, Crud, Likeable};
#[test] #[test]
fn test_crud() { fn test_crud() {
@ -575,6 +587,7 @@ mod tests {
published: inserted_comment.published, published: inserted_comment.published,
updated: None, updated: None,
creator_name: inserted_user.name.to_owned(), creator_name: inserted_user.name.to_owned(),
creator_published: inserted_user.published,
creator_avatar: None, creator_avatar: None,
score: 1, score: 1,
downvotes: 0, downvotes: 0,
@ -608,6 +621,7 @@ mod tests {
published: inserted_comment.published, published: inserted_comment.published,
updated: None, updated: None,
creator_name: inserted_user.name.to_owned(), creator_name: inserted_user.name.to_owned(),
creator_published: inserted_user.published,
creator_avatar: None, creator_avatar: None,
score: 1, score: 1,
downvotes: 0, downvotes: 0,
@ -615,8 +629,8 @@ mod tests {
upvotes: 1, upvotes: 1,
user_id: Some(inserted_user.id), user_id: Some(inserted_user.id),
my_vote: Some(1), my_vote: Some(1),
subscribed: None, subscribed: Some(false),
saved: None, saved: Some(false),
ap_id: "http://fake.com".to_string(), ap_id: "http://fake.com".to_string(),
local: true, local: true,
community_actor_id: inserted_community.actor_id.to_owned(), community_actor_id: inserted_community.actor_id.to_owned(),

View file

@ -1,6 +1,9 @@
use crate::{ use crate::{
db::{Bannable, Crud, Followable, Joinable},
schema::{community, community_follower, community_moderator, community_user_ban}, schema::{community, community_follower, community_moderator, community_user_ban},
Bannable,
Crud,
Followable,
Joinable,
}; };
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -232,8 +235,7 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{super::user::*, *}; use crate::{community::*, tests::establish_unpooled_connection, user::*, ListingType, SortType};
use crate::db::{establish_unpooled_connection, ListingType, SortType};
#[test] #[test]
fn test_crud() { fn test_crud() {

View file

@ -1,5 +1,5 @@
use super::community_view::community_mview::BoxedQuery; use super::community_view::community_fast_view::BoxedQuery;
use crate::db::{fuzzy_search, limit_and_offset, MaybeOptional, SortType}; use crate::{fuzzy_search, limit_and_offset, MaybeOptional, SortType};
use diesel::{pg::Pg, result::Error, *}; use diesel::{pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -34,7 +34,7 @@ table! {
} }
table! { table! {
community_mview (id) { community_fast_view (id) {
id -> Int4, id -> Int4,
name -> Varchar, name -> Varchar,
title -> Varchar, title -> Varchar,
@ -114,7 +114,7 @@ table! {
#[derive( #[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)] )]
#[table_name = "community_view"] #[table_name = "community_fast_view"]
pub struct CommunityView { pub struct CommunityView {
pub id: i32, pub id: i32,
pub name: String, pub name: String,
@ -156,9 +156,9 @@ pub struct CommunityQueryBuilder<'a> {
impl<'a> CommunityQueryBuilder<'a> { impl<'a> CommunityQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection) -> Self { pub fn create(conn: &'a PgConnection) -> Self {
use super::community_view::community_mview::dsl::*; use super::community_view::community_fast_view::dsl::*;
let query = community_mview.into_boxed(); let query = community_fast_view.into_boxed();
CommunityQueryBuilder { CommunityQueryBuilder {
conn, conn,
@ -203,7 +203,7 @@ impl<'a> CommunityQueryBuilder<'a> {
} }
pub fn list(self) -> Result<Vec<CommunityView>, Error> { pub fn list(self) -> Result<Vec<CommunityView>, Error> {
use super::community_view::community_mview::dsl::*; use super::community_view::community_fast_view::dsl::*;
let mut query = self.query; let mut query = self.query;
@ -259,9 +259,9 @@ impl CommunityView {
from_community_id: i32, from_community_id: i32,
from_user_id: Option<i32>, from_user_id: Option<i32>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
use super::community_view::community_mview::dsl::*; use super::community_view::community_fast_view::dsl::*;
let mut query = community_mview.into_boxed(); let mut query = community_fast_view.into_boxed();
query = query.filter(id.eq(from_community_id)); query = query.filter(id.eq(from_community_id));

View file

@ -1,10 +1,22 @@
use crate::settings::Settings; #[macro_use]
pub extern crate diesel;
#[macro_use]
pub extern crate strum_macros;
pub extern crate bcrypt;
pub extern crate chrono;
pub extern crate log;
pub extern crate serde;
pub extern crate serde_json;
pub extern crate sha2;
pub extern crate strum;
use chrono::NaiveDateTime;
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{env, env::VarError};
pub mod activity; pub mod activity;
pub mod category; pub mod category;
pub mod code_migrations;
pub mod comment; pub mod comment;
pub mod comment_view; pub mod comment_view;
pub mod community; pub mod community;
@ -16,6 +28,7 @@ pub mod post;
pub mod post_view; pub mod post_view;
pub mod private_message; pub mod private_message;
pub mod private_message_view; pub mod private_message_view;
pub mod schema;
pub mod site; pub mod site;
pub mod site_view; pub mod site_view;
pub mod user; pub mod user;
@ -111,9 +124,8 @@ impl<T> MaybeOptional<T> for Option<T> {
} }
} }
pub fn establish_unpooled_connection() -> PgConnection { pub fn get_database_url_from_env() -> Result<String, VarError> {
let db_url = Settings::get().get_database_url(); env::var("LEMMY_DATABASE_URL")
PgConnection::establish(&db_url).unwrap_or_else(|_| panic!("Error connecting to {}", db_url))
} }
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)] #[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
@ -155,9 +167,25 @@ pub fn limit_and_offset(page: Option<i64>, limit: Option<i64>) -> (i64, i64) {
let offset = limit * (page - 1); let offset = limit * (page - 1);
(limit, offset) (limit, offset)
} }
pub fn naive_now() -> NaiveDateTime {
chrono::prelude::Utc::now().naive_utc()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::fuzzy_search; use super::fuzzy_search;
use crate::get_database_url_from_env;
use diesel::{Connection, PgConnection};
pub fn establish_unpooled_connection() -> PgConnection {
let db_url = match get_database_url_from_env() {
Ok(url) => url,
Err(_) => panic!("Failed to read database URL from env var LEMMY_DATABASE_URL"),
};
PgConnection::establish(&db_url).unwrap_or_else(|_| panic!("Error connecting to {}", db_url))
}
#[test] #[test]
fn test_fuzzy_search() { fn test_fuzzy_search() {
let test = "This is a fuzzy search"; let test = "This is a fuzzy search";

View file

@ -1,5 +1,4 @@
use crate::{ use crate::{
db::Crud,
schema::{ schema::{
mod_add, mod_add,
mod_add_community, mod_add_community,
@ -11,6 +10,7 @@ use crate::{
mod_remove_post, mod_remove_post,
mod_sticky_post, mod_sticky_post,
}, },
Crud,
}; };
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -437,11 +437,16 @@ impl Crud<ModAddForm> for ModAdd {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use crate::{
super::{comment::*, community::*, post::*, user::*}, comment::*,
*, community::*,
moderator::*,
post::*,
tests::establish_unpooled_connection,
user::*,
ListingType,
SortType,
}; };
use crate::db::{establish_unpooled_connection, ListingType, SortType};
// use Crud; // use Crud;
#[test] #[test]

View file

@ -1,4 +1,4 @@
use crate::db::limit_and_offset; use crate::limit_and_offset;
use diesel::{result::Error, *}; use diesel::{result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
db::Crud,
schema::{password_reset_request, password_reset_request::dsl::*}, schema::{password_reset_request, password_reset_request::dsl::*},
Crud,
}; };
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@ -82,7 +82,7 @@ impl PasswordResetRequest {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{super::user::*, *}; use super::{super::user::*, *};
use crate::db::{establish_unpooled_connection, ListingType, SortType}; use crate::{tests::establish_unpooled_connection, ListingType, SortType};
#[test] #[test]
fn test_crud() { fn test_crud() {

View file

@ -1,8 +1,10 @@
use crate::{ use crate::{
apub::{make_apub_endpoint, EndpointType},
db::{Crud, Likeable, Readable, Saveable},
naive_now, naive_now,
schema::{post, post_like, post_read, post_saved}, schema::{post, post_like, post_read, post_saved},
Crud,
Likeable,
Readable,
Saveable,
}; };
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -75,12 +77,11 @@ impl Post {
post.filter(ap_id.eq(object_id)).first::<Self>(conn) post.filter(ap_id.eq(object_id)).first::<Self>(conn)
} }
pub fn update_ap_id(conn: &PgConnection, post_id: i32) -> Result<Self, Error> { pub fn update_ap_id(conn: &PgConnection, post_id: i32, apub_id: String) -> Result<Self, Error> {
use crate::schema::post::dsl::*; use crate::schema::post::dsl::*;
let apid = make_apub_endpoint(EndpointType::Post, &post_id.to_string()).to_string();
diesel::update(post.find(post_id)) diesel::update(post.find(post_id))
.set(ap_id.eq(apid)) .set(ap_id.eq(apub_id))
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
@ -241,11 +242,14 @@ impl Readable<PostReadForm> for PostRead {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use crate::{
super::{community::*, user::*}, community::*,
*, post::*,
tests::establish_unpooled_connection,
user::*,
ListingType,
SortType,
}; };
use crate::db::{establish_unpooled_connection, ListingType, SortType};
#[test] #[test]
fn test_crud() { fn test_crud() {

View file

@ -1,5 +1,5 @@
use super::post_view::post_mview::BoxedQuery; use super::post_view::post_fast_view::BoxedQuery;
use crate::db::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType}; use crate::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType};
use diesel::{dsl::*, pg::Pg, result::Error, *}; use diesel::{dsl::*, pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -25,12 +25,13 @@ table! {
thumbnail_url -> Nullable<Text>, thumbnail_url -> Nullable<Text>,
ap_id -> Text, ap_id -> Text,
local -> Bool, local -> Bool,
banned -> Bool,
banned_from_community -> Bool,
creator_actor_id -> Text, creator_actor_id -> Text,
creator_local -> Bool, creator_local -> Bool,
creator_name -> Varchar, creator_name -> Varchar,
creator_published -> Timestamp,
creator_avatar -> Nullable<Text>, creator_avatar -> Nullable<Text>,
banned -> Bool,
banned_from_community -> Bool,
community_actor_id -> Text, community_actor_id -> Text,
community_local -> Bool, community_local -> Bool,
community_name -> Varchar, community_name -> Varchar,
@ -52,7 +53,7 @@ table! {
} }
table! { table! {
post_mview (id) { post_fast_view (id) {
id -> Int4, id -> Int4,
name -> Varchar, name -> Varchar,
url -> Nullable<Text>, url -> Nullable<Text>,
@ -72,12 +73,13 @@ table! {
thumbnail_url -> Nullable<Text>, thumbnail_url -> Nullable<Text>,
ap_id -> Text, ap_id -> Text,
local -> Bool, local -> Bool,
banned -> Bool,
banned_from_community -> Bool,
creator_actor_id -> Text, creator_actor_id -> Text,
creator_local -> Bool, creator_local -> Bool,
creator_name -> Varchar, creator_name -> Varchar,
creator_published -> Timestamp,
creator_avatar -> Nullable<Text>, creator_avatar -> Nullable<Text>,
banned -> Bool,
banned_from_community -> Bool,
community_actor_id -> Text, community_actor_id -> Text,
community_local -> Bool, community_local -> Bool,
community_name -> Varchar, community_name -> Varchar,
@ -101,7 +103,7 @@ table! {
#[derive( #[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)] )]
#[table_name = "post_view"] #[table_name = "post_fast_view"]
pub struct PostView { pub struct PostView {
pub id: i32, pub id: i32,
pub name: String, pub name: String,
@ -122,12 +124,13 @@ pub struct PostView {
pub thumbnail_url: Option<String>, pub thumbnail_url: Option<String>,
pub ap_id: String, pub ap_id: String,
pub local: bool, pub local: bool,
pub banned: bool,
pub banned_from_community: bool,
pub creator_actor_id: String, pub creator_actor_id: String,
pub creator_local: bool, pub creator_local: bool,
pub creator_name: String, pub creator_name: String,
pub creator_published: chrono::NaiveDateTime,
pub creator_avatar: Option<String>, pub creator_avatar: Option<String>,
pub banned: bool,
pub banned_from_community: bool,
pub community_actor_id: String, pub community_actor_id: String,
pub community_local: bool, pub community_local: bool,
pub community_name: String, pub community_name: String,
@ -166,9 +169,9 @@ pub struct PostQueryBuilder<'a> {
impl<'a> PostQueryBuilder<'a> { impl<'a> PostQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection) -> Self { pub fn create(conn: &'a PgConnection) -> Self {
use super::post_view::post_mview::dsl::*; use super::post_view::post_fast_view::dsl::*;
let query = post_mview.into_boxed(); let query = post_fast_view.into_boxed();
PostQueryBuilder { PostQueryBuilder {
conn, conn,
@ -249,7 +252,7 @@ impl<'a> PostQueryBuilder<'a> {
} }
pub fn list(self) -> Result<Vec<PostView>, Error> { pub fn list(self) -> Result<Vec<PostView>, Error> {
use super::post_view::post_mview::dsl::*; use super::post_view::post_fast_view::dsl::*;
let mut query = self.query; let mut query = self.query;
@ -345,10 +348,10 @@ impl PostView {
from_post_id: i32, from_post_id: i32,
my_user_id: Option<i32>, my_user_id: Option<i32>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
use super::post_view::post_mview::dsl::*; use super::post_view::post_fast_view::dsl::*;
use diesel::prelude::*; use diesel::prelude::*;
let mut query = post_mview.into_boxed(); let mut query = post_fast_view.into_boxed();
query = query.filter(id.eq(from_post_id)); query = query.filter(id.eq(from_post_id));
@ -364,11 +367,16 @@ impl PostView {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use crate::{
super::{community::*, post::*, user::*}, community::*,
post::*,
post_view::*,
tests::establish_unpooled_connection,
user::*,
Crud,
Likeable,
*, *,
}; };
use crate::db::{establish_unpooled_connection, Crud, Likeable};
#[test] #[test]
fn test_crud() { fn test_crud() {
@ -470,6 +478,25 @@ mod tests {
score: 1, score: 1,
}; };
let read_post_listings_with_user = PostQueryBuilder::create(&conn)
.listing_type(ListingType::Community)
.sort(&SortType::New)
.for_community_id(inserted_community.id)
.my_user_id(inserted_user.id)
.list()
.unwrap();
let read_post_listings_no_user = PostQueryBuilder::create(&conn)
.listing_type(ListingType::Community)
.sort(&SortType::New)
.for_community_id(inserted_community.id)
.list()
.unwrap();
let read_post_listing_no_user = PostView::read(&conn, inserted_post.id, None).unwrap();
let read_post_listing_with_user =
PostView::read(&conn, inserted_post.id, Some(inserted_user.id)).unwrap();
// the non user version // the non user version
let expected_post_listing_no_user = PostView { let expected_post_listing_no_user = PostView {
user_id: None, user_id: None,
@ -480,6 +507,7 @@ mod tests {
body: None, body: None,
creator_id: inserted_user.id, creator_id: inserted_user.id,
creator_name: user_name.to_owned(), creator_name: user_name.to_owned(),
creator_published: inserted_user.published,
creator_avatar: None, creator_avatar: None,
banned: false, banned: false,
banned_from_community: false, banned_from_community: false,
@ -496,7 +524,7 @@ mod tests {
score: 1, score: 1,
upvotes: 1, upvotes: 1,
downvotes: 0, downvotes: 0,
hot_rank: 1728, hot_rank: read_post_listing_no_user.hot_rank,
published: inserted_post.published, published: inserted_post.published,
newest_activity_time: inserted_post.published, newest_activity_time: inserted_post.published,
updated: None, updated: None,
@ -529,6 +557,7 @@ mod tests {
stickied: false, stickied: false,
creator_id: inserted_user.id, creator_id: inserted_user.id,
creator_name: user_name, creator_name: user_name,
creator_published: inserted_user.published,
creator_avatar: None, creator_avatar: None,
banned: false, banned: false,
banned_from_community: false, banned_from_community: false,
@ -541,13 +570,13 @@ mod tests {
score: 1, score: 1,
upvotes: 1, upvotes: 1,
downvotes: 0, downvotes: 0,
hot_rank: 1728, hot_rank: read_post_listing_with_user.hot_rank,
published: inserted_post.published, published: inserted_post.published,
newest_activity_time: inserted_post.published, newest_activity_time: inserted_post.published,
updated: None, updated: None,
subscribed: None, subscribed: Some(false),
read: None, read: Some(false),
saved: None, saved: Some(false),
nsfw: false, nsfw: false,
embed_title: None, embed_title: None,
embed_description: None, embed_description: None,
@ -561,25 +590,6 @@ mod tests {
community_local: true, community_local: true,
}; };
let read_post_listings_with_user = PostQueryBuilder::create(&conn)
.listing_type(ListingType::Community)
.sort(&SortType::New)
.for_community_id(inserted_community.id)
.my_user_id(inserted_user.id)
.list()
.unwrap();
let read_post_listings_no_user = PostQueryBuilder::create(&conn)
.listing_type(ListingType::Community)
.sort(&SortType::New)
.for_community_id(inserted_community.id)
.list()
.unwrap();
let read_post_listing_no_user = PostView::read(&conn, inserted_post.id, None).unwrap();
let read_post_listing_with_user =
PostView::read(&conn, inserted_post.id, Some(inserted_user.id)).unwrap();
let like_removed = PostLike::remove(&conn, &post_like_form).unwrap(); let like_removed = PostLike::remove(&conn, &post_like_form).unwrap();
let num_deleted = Post::delete(&conn, inserted_post.id).unwrap(); let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
Community::delete(&conn, inserted_community.id).unwrap(); Community::delete(&conn, inserted_community.id).unwrap();

View file

@ -1,8 +1,4 @@
use crate::{ use crate::{schema::private_message, Crud};
apub::{make_apub_endpoint, EndpointType},
db::Crud,
schema::private_message,
};
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -66,16 +62,15 @@ impl Crud<PrivateMessageForm> for PrivateMessage {
} }
impl PrivateMessage { impl PrivateMessage {
pub fn update_ap_id(conn: &PgConnection, private_message_id: i32) -> Result<Self, Error> { pub fn update_ap_id(
conn: &PgConnection,
private_message_id: i32,
apub_id: String,
) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*; use crate::schema::private_message::dsl::*;
let apid = make_apub_endpoint(
EndpointType::PrivateMessage,
&private_message_id.to_string(),
)
.to_string();
diesel::update(private_message.find(private_message_id)) diesel::update(private_message.find(private_message_id))
.set(ap_id.eq(apid)) .set(ap_id.eq(apub_id))
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
@ -89,8 +84,13 @@ impl PrivateMessage {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{super::user::*, *}; use crate::{
use crate::db::{establish_unpooled_connection, ListingType, SortType}; private_message::*,
tests::establish_unpooled_connection,
user::*,
ListingType,
SortType,
};
#[test] #[test]
fn test_crud() { fn test_crud() {

View file

@ -1,4 +1,4 @@
use crate::db::{limit_and_offset, MaybeOptional}; use crate::{limit_and_offset, MaybeOptional};
use diesel::{pg::Pg, result::Error, *}; use diesel::{pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -26,29 +26,6 @@ table! {
} }
} }
table! {
private_message_mview (id) {
id -> Int4,
creator_id -> Int4,
recipient_id -> Int4,
content -> Text,
deleted -> Bool,
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
ap_id -> Text,
local -> Bool,
creator_name -> Varchar,
creator_avatar -> Nullable<Text>,
creator_actor_id -> Text,
creator_local -> Bool,
recipient_name -> Varchar,
recipient_avatar -> Nullable<Text>,
recipient_actor_id -> Text,
recipient_local -> Bool,
}
}
#[derive( #[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)] )]
@ -76,7 +53,7 @@ pub struct PrivateMessageView {
pub struct PrivateMessageQueryBuilder<'a> { pub struct PrivateMessageQueryBuilder<'a> {
conn: &'a PgConnection, conn: &'a PgConnection,
query: super::private_message_view::private_message_mview::BoxedQuery<'a, Pg>, query: super::private_message_view::private_message_view::BoxedQuery<'a, Pg>,
for_recipient_id: i32, for_recipient_id: i32,
unread_only: bool, unread_only: bool,
page: Option<i64>, page: Option<i64>,
@ -85,9 +62,9 @@ pub struct PrivateMessageQueryBuilder<'a> {
impl<'a> PrivateMessageQueryBuilder<'a> { impl<'a> PrivateMessageQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection, for_recipient_id: i32) -> Self { pub fn create(conn: &'a PgConnection, for_recipient_id: i32) -> Self {
use super::private_message_view::private_message_mview::dsl::*; use super::private_message_view::private_message_view::dsl::*;
let query = private_message_mview.into_boxed(); let query = private_message_view.into_boxed();
PrivateMessageQueryBuilder { PrivateMessageQueryBuilder {
conn, conn,
@ -115,7 +92,7 @@ impl<'a> PrivateMessageQueryBuilder<'a> {
} }
pub fn list(self) -> Result<Vec<PrivateMessageView>, Error> { pub fn list(self) -> Result<Vec<PrivateMessageView>, Error> {
use super::private_message_view::private_message_mview::dsl::*; use super::private_message_view::private_message_view::dsl::*;
let mut query = self.query.filter(deleted.eq(false)); let mut query = self.query.filter(deleted.eq(false));

View file

@ -33,6 +33,38 @@ table! {
} }
} }
table! {
comment_aggregates_fast (id) {
id -> Int4,
creator_id -> Nullable<Int4>,
post_id -> Nullable<Int4>,
parent_id -> Nullable<Int4>,
content -> Nullable<Text>,
removed -> Nullable<Bool>,
read -> Nullable<Bool>,
published -> Nullable<Timestamp>,
updated -> Nullable<Timestamp>,
deleted -> Nullable<Bool>,
ap_id -> Nullable<Varchar>,
local -> Nullable<Bool>,
community_id -> Nullable<Int4>,
community_actor_id -> Nullable<Varchar>,
community_local -> Nullable<Bool>,
community_name -> Nullable<Varchar>,
banned -> Nullable<Bool>,
banned_from_community -> Nullable<Bool>,
creator_actor_id -> Nullable<Varchar>,
creator_local -> Nullable<Bool>,
creator_name -> Nullable<Varchar>,
creator_published -> Nullable<Timestamp>,
creator_avatar -> Nullable<Text>,
score -> Nullable<Int8>,
upvotes -> Nullable<Int8>,
downvotes -> Nullable<Int8>,
hot_rank -> Nullable<Int4>,
}
}
table! { table! {
comment_like (id) { comment_like (id) {
id -> Int4, id -> Int4,
@ -74,6 +106,34 @@ table! {
} }
} }
table! {
community_aggregates_fast (id) {
id -> Int4,
name -> Nullable<Varchar>,
title -> Nullable<Varchar>,
description -> Nullable<Text>,
category_id -> Nullable<Int4>,
creator_id -> Nullable<Int4>,
removed -> Nullable<Bool>,
published -> Nullable<Timestamp>,
updated -> Nullable<Timestamp>,
deleted -> Nullable<Bool>,
nsfw -> Nullable<Bool>,
actor_id -> Nullable<Varchar>,
local -> Nullable<Bool>,
last_refreshed_at -> Nullable<Timestamp>,
creator_actor_id -> Nullable<Varchar>,
creator_local -> Nullable<Bool>,
creator_name -> Nullable<Varchar>,
creator_avatar -> Nullable<Text>,
category_name -> Nullable<Varchar>,
number_of_subscribers -> Nullable<Int8>,
number_of_posts -> Nullable<Int8>,
number_of_comments -> Nullable<Int8>,
hot_rank -> Nullable<Int4>,
}
}
table! { table! {
community_follower (id) { community_follower (id) {
id -> Int4, id -> Int4,
@ -234,6 +294,49 @@ table! {
} }
} }
table! {
post_aggregates_fast (id) {
id -> Int4,
name -> Nullable<Varchar>,
url -> Nullable<Text>,
body -> Nullable<Text>,
creator_id -> Nullable<Int4>,
community_id -> Nullable<Int4>,
removed -> Nullable<Bool>,
locked -> Nullable<Bool>,
published -> Nullable<Timestamp>,
updated -> Nullable<Timestamp>,
deleted -> Nullable<Bool>,
nsfw -> Nullable<Bool>,
stickied -> Nullable<Bool>,
embed_title -> Nullable<Text>,
embed_description -> Nullable<Text>,
embed_html -> Nullable<Text>,
thumbnail_url -> Nullable<Text>,
ap_id -> Nullable<Varchar>,
local -> Nullable<Bool>,
creator_actor_id -> Nullable<Varchar>,
creator_local -> Nullable<Bool>,
creator_name -> Nullable<Varchar>,
creator_published -> Nullable<Timestamp>,
creator_avatar -> Nullable<Text>,
banned -> Nullable<Bool>,
banned_from_community -> Nullable<Bool>,
community_actor_id -> Nullable<Varchar>,
community_local -> Nullable<Bool>,
community_name -> Nullable<Varchar>,
community_removed -> Nullable<Bool>,
community_deleted -> Nullable<Bool>,
community_nsfw -> Nullable<Bool>,
number_of_comments -> Nullable<Int8>,
score -> Nullable<Int8>,
upvotes -> Nullable<Int8>,
downvotes -> Nullable<Int8>,
hot_rank -> Nullable<Int4>,
newest_activity_time -> Nullable<Timestamp>,
}
}
table! { table! {
post_like (id) { post_like (id) {
id -> Int4, id -> Int4,
@ -328,6 +431,28 @@ table! {
} }
} }
table! {
user_fast (id) {
id -> Int4,
actor_id -> Nullable<Varchar>,
name -> Nullable<Varchar>,
avatar -> Nullable<Text>,
email -> Nullable<Text>,
matrix_user_id -> Nullable<Text>,
bio -> Nullable<Text>,
local -> Nullable<Bool>,
admin -> Nullable<Bool>,
banned -> Nullable<Bool>,
show_avatars -> Nullable<Bool>,
send_notifications_to_email -> Nullable<Bool>,
published -> Nullable<Timestamp>,
number_of_posts -> Nullable<Int8>,
post_score -> Nullable<Int8>,
number_of_comments -> Nullable<Int8>,
comment_score -> Nullable<Int8>,
}
}
table! { table! {
user_mention (id) { user_mention (id) {
id -> Int4, id -> Int4,
@ -384,9 +509,11 @@ allow_tables_to_appear_in_same_query!(
activity, activity,
category, category,
comment, comment,
comment_aggregates_fast,
comment_like, comment_like,
comment_saved, comment_saved,
community, community,
community_aggregates_fast,
community_follower, community_follower,
community_moderator, community_moderator,
community_user_ban, community_user_ban,
@ -401,6 +528,7 @@ allow_tables_to_appear_in_same_query!(
mod_sticky_post, mod_sticky_post,
password_reset_request, password_reset_request,
post, post,
post_aggregates_fast,
post_like, post_like,
post_read, post_read,
post_saved, post_saved,
@ -408,5 +536,6 @@ allow_tables_to_appear_in_same_query!(
site, site,
user_, user_,
user_ban, user_ban,
user_fast,
user_mention, user_mention,
); );

View file

@ -1,4 +1,4 @@
use crate::{db::Crud, schema::site}; use crate::{schema::site, Crud};
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View file

@ -1,14 +1,10 @@
use crate::{ use crate::{
db::Crud,
is_email_regex,
naive_now, naive_now,
schema::{user_, user_::dsl::*}, schema::{user_, user_::dsl::*},
settings::Settings, Crud,
}; };
use bcrypt::{hash, DEFAULT_COST}; use bcrypt::{hash, DEFAULT_COST};
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
use serde::{Deserialize, Serialize};
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug)] #[derive(Clone, Queryable, Identifiable, PartialEq, Debug)]
#[table_name = "user_"] #[table_name = "user_"]
@ -131,90 +127,23 @@ impl User_ {
} }
} }
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub id: i32,
pub username: String,
pub iss: String,
pub show_nsfw: bool,
pub theme: String,
pub default_sort_type: i16,
pub default_listing_type: i16,
pub lang: String,
pub avatar: Option<String>,
pub show_avatars: bool,
}
impl Claims {
pub fn decode(jwt: &str) -> Result<TokenData<Claims>, jsonwebtoken::errors::Error> {
let v = Validation {
validate_exp: false,
..Validation::default()
};
decode::<Claims>(
&jwt,
&DecodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
&v,
)
}
}
type Jwt = String;
impl User_ { impl User_ {
pub fn jwt(&self) -> Jwt { pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<User_, Error> {
let my_claims = Claims {
id: self.id,
username: self.name.to_owned(),
iss: Settings::get().hostname,
show_nsfw: self.show_nsfw,
theme: self.theme.to_owned(),
default_sort_type: self.default_sort_type,
default_listing_type: self.default_listing_type,
lang: self.lang.to_owned(),
avatar: self.avatar.to_owned(),
show_avatars: self.show_avatars.to_owned(),
};
encode(
&Header::default(),
&my_claims,
&EncodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
)
.unwrap()
}
pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<Self, Error> {
user_.filter(name.eq(username)).first::<User_>(conn) user_.filter(name.eq(username)).first::<User_>(conn)
} }
pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<Self, Error> { pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<User_, Error> {
user_.filter(email.eq(from_email)).first::<User_>(conn) user_.filter(email.eq(from_email)).first::<User_>(conn)
} }
pub fn find_by_email_or_username( pub fn get_profile_url(&self, hostname: &str) -> String {
conn: &PgConnection, format!("https://{}/u/{}", hostname, self.name)
username_or_email: &str,
) -> Result<Self, Error> {
if is_email_regex(username_or_email) {
User_::find_by_email(conn, username_or_email)
} else {
User_::find_by_username(conn, username_or_email)
}
}
pub fn get_profile_url(&self) -> String {
format!("https://{}/u/{}", Settings::get().hostname, self.name)
}
pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<Self, Error> {
let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims;
Self::read(&conn, claims.id)
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{User_, *}; use crate::{tests::establish_unpooled_connection, user::*, ListingType, SortType};
use crate::db::{establish_unpooled_connection, ListingType, SortType};
#[test] #[test]
fn test_crud() { fn test_crud() {

View file

@ -1,5 +1,5 @@
use super::comment::Comment; use super::comment::Comment;
use crate::{db::Crud, schema::user_mention}; use crate::{schema::user_mention, Crud};
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -54,11 +54,16 @@ impl Crud<UserMentionForm> for UserMention {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use crate::{
super::{comment::*, community::*, post::*, user::*}, comment::*,
*, community::*,
post::*,
tests::establish_unpooled_connection,
user::*,
user_mention::*,
ListingType,
SortType,
}; };
use crate::db::{establish_unpooled_connection, ListingType, SortType};
#[test] #[test]
fn test_crud() { fn test_crud() {

View file

@ -1,4 +1,4 @@
use crate::db::{limit_and_offset, MaybeOptional, SortType}; use crate::{limit_and_offset, MaybeOptional, SortType};
use diesel::{dsl::*, pg::Pg, result::Error, *}; use diesel::{dsl::*, pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -40,7 +40,7 @@ table! {
} }
table! { table! {
user_mention_mview (id) { user_mention_fast_view (id) {
id -> Int4, id -> Int4,
user_mention_id -> Int4, user_mention_id -> Int4,
creator_id -> Int4, creator_id -> Int4,
@ -78,7 +78,7 @@ table! {
#[derive( #[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)] )]
#[table_name = "user_mention_view"] #[table_name = "user_mention_fast_view"]
pub struct UserMentionView { pub struct UserMentionView {
pub id: i32, pub id: i32,
pub user_mention_id: i32, pub user_mention_id: i32,
@ -115,7 +115,7 @@ pub struct UserMentionView {
pub struct UserMentionQueryBuilder<'a> { pub struct UserMentionQueryBuilder<'a> {
conn: &'a PgConnection, conn: &'a PgConnection,
query: super::user_mention_view::user_mention_mview::BoxedQuery<'a, Pg>, query: super::user_mention_view::user_mention_fast_view::BoxedQuery<'a, Pg>,
for_user_id: i32, for_user_id: i32,
sort: &'a SortType, sort: &'a SortType,
unread_only: bool, unread_only: bool,
@ -125,9 +125,9 @@ pub struct UserMentionQueryBuilder<'a> {
impl<'a> UserMentionQueryBuilder<'a> { impl<'a> UserMentionQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection, for_user_id: i32) -> Self { pub fn create(conn: &'a PgConnection, for_user_id: i32) -> Self {
use super::user_mention_view::user_mention_mview::dsl::*; use super::user_mention_view::user_mention_fast_view::dsl::*;
let query = user_mention_mview.into_boxed(); let query = user_mention_fast_view.into_boxed();
UserMentionQueryBuilder { UserMentionQueryBuilder {
conn, conn,
@ -161,7 +161,7 @@ impl<'a> UserMentionQueryBuilder<'a> {
} }
pub fn list(self) -> Result<Vec<UserMentionView>, Error> { pub fn list(self) -> Result<Vec<UserMentionView>, Error> {
use super::user_mention_view::user_mention_mview::dsl::*; use super::user_mention_view::user_mention_fast_view::dsl::*;
let mut query = self.query; let mut query = self.query;
@ -208,9 +208,9 @@ impl UserMentionView {
from_user_mention_id: i32, from_user_mention_id: i32,
from_recipient_id: i32, from_recipient_id: i32,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
use super::user_mention_view::user_mention_view::dsl::*; use super::user_mention_view::user_mention_fast_view::dsl::*;
user_mention_view user_mention_fast_view
.filter(user_mention_id.eq(from_user_mention_id)) .filter(user_mention_id.eq(from_user_mention_id))
.filter(user_id.eq(from_recipient_id)) .filter(user_id.eq(from_recipient_id))
.first::<Self>(conn) .first::<Self>(conn)

View file

@ -1,5 +1,5 @@
use super::user_view::user_mview::BoxedQuery; use super::user_view::user_fast::BoxedQuery;
use crate::db::{fuzzy_search, limit_and_offset, MaybeOptional, SortType}; use crate::{fuzzy_search, limit_and_offset, MaybeOptional, SortType};
use diesel::{dsl::*, pg::Pg, result::Error, *}; use diesel::{dsl::*, pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -26,7 +26,7 @@ table! {
} }
table! { table! {
user_mview (id) { user_fast (id) {
id -> Int4, id -> Int4,
actor_id -> Text, actor_id -> Text,
name -> Varchar, name -> Varchar,
@ -50,7 +50,7 @@ table! {
#[derive( #[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)] )]
#[table_name = "user_view"] #[table_name = "user_fast"]
pub struct UserView { pub struct UserView {
pub id: i32, pub id: i32,
pub actor_id: String, pub actor_id: String,
@ -81,9 +81,9 @@ pub struct UserQueryBuilder<'a> {
impl<'a> UserQueryBuilder<'a> { impl<'a> UserQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection) -> Self { pub fn create(conn: &'a PgConnection) -> Self {
use super::user_view::user_mview::dsl::*; use super::user_view::user_fast::dsl::*;
let query = user_mview.into_boxed(); let query = user_fast.into_boxed();
UserQueryBuilder { UserQueryBuilder {
conn, conn,
@ -100,7 +100,7 @@ impl<'a> UserQueryBuilder<'a> {
} }
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self { pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
use super::user_view::user_mview::dsl::*; use super::user_view::user_fast::dsl::*;
if let Some(search_term) = search_term.get_optional() { if let Some(search_term) = search_term.get_optional() {
self.query = self.query.filter(name.ilike(fuzzy_search(&search_term))); self.query = self.query.filter(name.ilike(fuzzy_search(&search_term)));
} }
@ -118,7 +118,7 @@ impl<'a> UserQueryBuilder<'a> {
} }
pub fn list(self) -> Result<Vec<UserView>, Error> { pub fn list(self) -> Result<Vec<UserView>, Error> {
use super::user_view::user_mview::dsl::*; use super::user_view::user_fast::dsl::*;
let mut query = self.query; let mut query = self.query;
@ -151,17 +151,17 @@ impl<'a> UserQueryBuilder<'a> {
impl UserView { impl UserView {
pub fn read(conn: &PgConnection, from_user_id: i32) -> Result<Self, Error> { pub fn read(conn: &PgConnection, from_user_id: i32) -> Result<Self, Error> {
use super::user_view::user_mview::dsl::*; use super::user_view::user_fast::dsl::*;
user_mview.find(from_user_id).first::<Self>(conn) user_fast.find(from_user_id).first::<Self>(conn)
} }
pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> { pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
use super::user_view::user_mview::dsl::*; use super::user_view::user_fast::dsl::*;
user_mview.filter(admin.eq(true)).load::<Self>(conn) user_fast.filter(admin.eq(true)).load::<Self>(conn)
} }
pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> { pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> {
use super::user_view::user_mview::dsl::*; use super::user_view::user_fast::dsl::*;
user_mview.filter(banned.eq(true)).load::<Self>(conn) user_fast.filter(banned.eq(true)).load::<Self>(conn)
} }
} }

22
server/lemmy_utils/Cargo.toml vendored Normal file
View file

@ -0,0 +1,22 @@
[package]
name = "lemmy_utils"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
regex = "1.3.5"
config = { version = "0.10.1", default-features = false, features = ["hjson"] }
chrono = { version = "0.4.7", features = ["serde"] }
lettre = "0.9.3"
lettre_email = "0.9.4"
log = "0.4.0"
itertools = "0.9.0"
rand = "0.7.3"
serde = { version = "1.0.105", features = ["derive"] }
serde_json = { version = "1.0.52", features = ["preserve_order"]}
comrak = "0.7"
lazy_static = "1.3.0"
openssl = "0.10"
url = { version = "2.1.1", features = ["serde"] }

View file

@ -0,0 +1,339 @@
#[macro_use]
pub extern crate lazy_static;
pub extern crate comrak;
pub extern crate lettre;
pub extern crate lettre_email;
pub extern crate openssl;
pub extern crate rand;
pub extern crate regex;
pub extern crate serde_json;
pub extern crate url;
pub mod settings;
use crate::settings::Settings;
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc};
use itertools::Itertools;
use lettre::{
smtp::{
authentication::{Credentials, Mechanism},
extension::ClientId,
ConnectionReuseParameters,
},
ClientSecurity,
SmtpClient,
Transport,
};
use lettre_email::Email;
use openssl::{pkey::PKey, rsa::Rsa};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use regex::{Regex, RegexBuilder};
use std::io::{Error, ErrorKind};
use url::Url;
pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
DateTime::<Utc>::from_utc(ndt, Utc)
}
pub fn naive_from_unix(time: i64) -> NaiveDateTime {
NaiveDateTime::from_timestamp(time, 0)
}
pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime<FixedOffset> {
let now = Local::now();
DateTime::<FixedOffset>::from_utc(datetime, *now.offset())
}
pub fn is_email_regex(test: &str) -> bool {
EMAIL_REGEX.is_match(test)
}
pub fn remove_slurs(test: &str) -> String {
SLUR_REGEX.replace_all(test, "*removed*").to_string()
}
pub fn slur_check(test: &str) -> Result<(), Vec<&str>> {
let mut matches: Vec<&str> = SLUR_REGEX.find_iter(test).map(|mat| mat.as_str()).collect();
// Unique
matches.sort_unstable();
matches.dedup();
if matches.is_empty() {
Ok(())
} else {
Err(matches)
}
}
pub fn slurs_vec_to_str(slurs: Vec<&str>) -> String {
let start = "No slurs - ";
let combined = &slurs.join(", ");
[start, combined].concat()
}
pub fn generate_random_string() -> String {
thread_rng().sample_iter(&Alphanumeric).take(30).collect()
}
pub fn send_email(
subject: &str,
to_email: &str,
to_username: &str,
html: &str,
) -> Result<(), String> {
let email_config = Settings::get().email.ok_or("no_email_setup")?;
let email = Email::builder()
.to((to_email, to_username))
.from(email_config.smtp_from_address.to_owned())
.subject(subject)
.html(html)
.build()
.unwrap();
let mailer = if email_config.use_tls {
SmtpClient::new_simple(&email_config.smtp_server).unwrap()
} else {
SmtpClient::new(&email_config.smtp_server, ClientSecurity::None).unwrap()
}
.hello_name(ClientId::Domain(Settings::get().hostname))
.smtp_utf8(true)
.authentication_mechanism(Mechanism::Plain)
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited);
let mailer = if let (Some(login), Some(password)) =
(&email_config.smtp_login, &email_config.smtp_password)
{
mailer.credentials(Credentials::new(login.to_owned(), password.to_owned()))
} else {
mailer
};
let mut transport = mailer.transport();
let result = transport.send(email.into());
transport.close();
match result {
Ok(_) => Ok(()),
Err(e) => Err(e.to_string()),
}
}
pub fn markdown_to_html(text: &str) -> String {
comrak::markdown_to_html(text, &comrak::ComrakOptions::default())
}
// TODO nothing is done with community / group webfingers yet, so just ignore those for now
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct MentionData {
pub name: String,
pub domain: String,
}
impl MentionData {
pub fn is_local(&self) -> bool {
Settings::get().hostname.eq(&self.domain)
}
pub fn full_name(&self) -> String {
format!("@{}@{}", &self.name, &self.domain)
}
}
pub fn scrape_text_for_mentions(text: &str) -> Vec<MentionData> {
let mut out: Vec<MentionData> = Vec::new();
for caps in MENTIONS_REGEX.captures_iter(text) {
out.push(MentionData {
name: caps["name"].to_string(),
domain: caps["domain"].to_string(),
});
}
out.into_iter().unique().collect()
}
pub fn is_valid_username(name: &str) -> bool {
VALID_USERNAME_REGEX.is_match(name)
}
pub fn is_valid_community_name(name: &str) -> bool {
VALID_COMMUNITY_NAME_REGEX.is_match(name)
}
pub fn is_valid_post_title(title: &str) -> bool {
VALID_POST_TITLE_REGEX.is_match(title)
}
#[cfg(test)]
mod tests {
use crate::{
is_email_regex,
is_valid_community_name,
is_valid_username,
is_valid_post_title,
remove_slurs,
scrape_text_for_mentions,
slur_check,
slurs_vec_to_str,
};
#[test]
fn test_mentions_regex() {
let text = "Just read a great blog post by [@tedu@honk.teduangst.com](/u/test). And another by !test_community@fish.teduangst.com . Another [@lemmy@lemmy-alpha:8540](/u/fish)";
let mentions = scrape_text_for_mentions(text);
assert_eq!(mentions[0].name, "tedu".to_string());
assert_eq!(mentions[0].domain, "honk.teduangst.com".to_string());
assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string());
}
#[test]
fn test_email() {
assert!(is_email_regex("gush@gmail.com"));
assert!(!is_email_regex("nada_neutho"));
}
#[test]
fn test_valid_register_username() {
assert!(is_valid_username("Hello_98"));
assert!(is_valid_username("ten"));
assert!(!is_valid_username("Hello-98"));
assert!(!is_valid_username("a"));
assert!(!is_valid_username(""));
}
#[test]
fn test_valid_community_name() {
assert!(is_valid_community_name("example"));
assert!(is_valid_community_name("example_community"));
assert!(!is_valid_community_name("Example"));
assert!(!is_valid_community_name("Ex"));
assert!(!is_valid_community_name(""));
}
#[test]
fn test_valid_post_title() {
assert!(is_valid_post_title("Post Title"));
assert!(is_valid_post_title(" POST TITLE 😃😃😃😃😃"));
assert!(!is_valid_post_title("\n \n \n \n ")); // tabs/spaces/newlines
}
#[test]
fn test_slur_filter() {
let test =
"coons test dindu ladyboy tranny retardeds. Capitalized Niggerz. This is a bunch of other safe text.";
let slur_free = "No slurs here";
assert_eq!(
remove_slurs(&test),
"*removed* test *removed* *removed* *removed* *removed*. Capitalized *removed*. This is a bunch of other safe text."
.to_string()
);
let has_slurs_vec = vec![
"Niggerz",
"coons",
"dindu",
"ladyboy",
"retardeds",
"tranny",
];
let has_slurs_err_str = "No slurs - Niggerz, coons, dindu, ladyboy, retardeds, tranny";
assert_eq!(slur_check(test), Err(has_slurs_vec));
assert_eq!(slur_check(slur_free), Ok(()));
if let Err(slur_vec) = slur_check(test) {
assert_eq!(&slurs_vec_to_str(slur_vec), has_slurs_err_str);
}
}
// These helped with testing
// #[test]
// fn test_send_email() {
// let result = send_email("not a subject", "test_email@gmail.com", "ur user", "<h1>HI there</h1>");
// assert!(result.is_ok());
// }
}
lazy_static! {
static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
static ref SLUR_REGEX: Regex = RegexBuilder::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|\bn(i|1)g(\b|g?(a|er)?(s|z)?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btr(a|@)nn?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap();
static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
// TODO keep this old one, it didn't work with port well tho
// static ref MENTIONS_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)").unwrap();
static ref MENTIONS_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._:-]+)").unwrap();
static ref VALID_USERNAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]{3,20}$").unwrap();
static ref VALID_COMMUNITY_NAME_REGEX: Regex = Regex::new(r"^[a-z0-9_]{3,20}$").unwrap();
static ref VALID_POST_TITLE_REGEX: Regex = Regex::new(r".*\S.*").unwrap();
pub static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!(
"^group:([a-z0-9_]{{3, 20}})@{}$",
Settings::get().hostname
))
.unwrap();
pub static ref WEBFINGER_USER_REGEX: Regex = Regex::new(&format!(
"^acct:([a-z0-9_]{{3, 20}})@{}$",
Settings::get().hostname
))
.unwrap();
pub static ref CACHE_CONTROL_REGEX: Regex =
Regex::new("^((text|image)/.+|application/javascript)$").unwrap();
}
pub struct Keypair {
pub private_key: String,
pub public_key: String,
}
/// Generate the asymmetric keypair for ActivityPub HTTP signatures.
pub fn generate_actor_keypair() -> Result<Keypair, Error> {
let rsa = Rsa::generate(2048)?;
let pkey = PKey::from_rsa(rsa)?;
let public_key = pkey.public_key_to_pem()?;
let private_key = pkey.private_key_to_pem_pkcs8()?;
let key_to_string = |key| match String::from_utf8(key) {
Ok(s) => Ok(s),
Err(e) => Err(Error::new(
ErrorKind::Other,
format!("Failed converting key to string: {}", e),
)),
};
Ok(Keypair {
private_key: key_to_string(private_key)?,
public_key: key_to_string(public_key)?,
})
}
pub enum EndpointType {
Community,
User,
Post,
Comment,
PrivateMessage,
}
pub fn get_apub_protocol_string() -> &'static str {
if Settings::get().federation.tls_enabled {
"https"
} else {
"http"
}
}
/// Generates the ActivityPub ID for a given object type and ID.
pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
let point = match endpoint_type {
EndpointType::Community => "c",
EndpointType::User => "u",
EndpointType::Post => "post",
EndpointType::Comment => "comment",
EndpointType::PrivateMessage => "private_message",
};
Url::parse(&format!(
"{}://{}/{}/{}",
get_apub_protocol_string(),
Settings::get().hostname,
point,
name
))
.unwrap()
}

View file

@ -1,7 +1,6 @@
use crate::LemmyError;
use config::{Config, ConfigError, Environment, File}; use config::{Config, ConfigError, Environment, File};
use serde::Deserialize; use serde::Deserialize;
use std::{env, fs, net::IpAddr, sync::RwLock}; use std::{fs, io::Error, net::IpAddr, sync::RwLock};
static CONFIG_FILE_DEFAULTS: &str = "config/defaults.hjson"; static CONFIG_FILE_DEFAULTS: &str = "config/defaults.hjson";
static CONFIG_FILE: &str = "config/config.hjson"; static CONFIG_FILE: &str = "config/config.hjson";
@ -76,6 +75,9 @@ impl Settings {
/// First, defaults are loaded from CONFIG_FILE_DEFAULTS, then these values can be overwritten /// First, defaults are loaded from CONFIG_FILE_DEFAULTS, then these values can be overwritten
/// from CONFIG_FILE (optional). Finally, values from the environment (with prefix LEMMY) are /// from CONFIG_FILE (optional). Finally, values from the environment (with prefix LEMMY) are
/// added to the config. /// added to the config.
///
/// Note: The env var `LEMMY_DATABASE_URL` is parsed in
/// `server/lemmy_db/src/lib.rs::get_database_url_from_env()`
fn init() -> Result<Self, ConfigError> { fn init() -> Result<Self, ConfigError> {
let mut s = Config::new(); let mut s = Config::new();
@ -98,31 +100,26 @@ impl Settings {
SETTINGS.read().unwrap().to_owned() SETTINGS.read().unwrap().to_owned()
} }
/// Returns the postgres connection url. If LEMMY_DATABASE_URL is set, that is used,
/// otherwise the connection url is generated from the config.
pub fn get_database_url(&self) -> String { pub fn get_database_url(&self) -> String {
match env::var("LEMMY_DATABASE_URL") { format!(
Ok(url) => url, "postgres://{}:{}@{}:{}/{}",
Err(_) => format!( self.database.user,
"postgres://{}:{}@{}:{}/{}", self.database.password,
self.database.user, self.database.host,
self.database.password, self.database.port,
self.database.host, self.database.database
self.database.port, )
self.database.database
),
}
} }
pub fn api_endpoint(&self) -> String { pub fn api_endpoint(&self) -> String {
format!("{}/api/v1", self.hostname) format!("{}/api/v1", self.hostname)
} }
pub fn read_config_file() -> Result<String, LemmyError> { pub fn read_config_file() -> Result<String, Error> {
Ok(fs::read_to_string(CONFIG_FILE)?) fs::read_to_string(CONFIG_FILE)
} }
pub fn save_config_file(data: &str) -> Result<String, LemmyError> { pub fn save_config_file(data: &str) -> Result<String, Error> {
fs::write(CONFIG_FILE, data)?; fs::write(CONFIG_FILE, data)?;
// Reload the new settings // Reload the new settings

View file

@ -0,0 +1,535 @@
-- Dropping all the fast tables
drop table user_fast;
drop view post_fast_view;
drop table post_aggregates_fast;
drop view community_fast_view;
drop table community_aggregates_fast;
drop view reply_fast_view;
drop view user_mention_fast_view;
drop view comment_fast_view;
drop table comment_aggregates_fast;
-- Re-adding all the triggers, functions, and mviews
-- private message
create materialized view private_message_mview as select * from private_message_view;
create unique index idx_private_message_mview_id on private_message_mview (id);
-- Create the triggers
create or replace function refresh_private_message()
returns trigger language plpgsql
as $$
begin
refresh materialized view concurrently private_message_mview;
return null;
end $$;
create trigger refresh_private_message
after insert or update or delete or truncate
on private_message
for each statement
execute procedure refresh_private_message();
-- user
create or replace function refresh_user()
returns trigger language plpgsql
as $$
begin
refresh materialized view concurrently user_mview;
refresh materialized view concurrently comment_aggregates_mview; -- cause of bans
refresh materialized view concurrently post_aggregates_mview;
return null;
end $$;
drop trigger refresh_user on user_;
create trigger refresh_user
after insert or update or delete or truncate
on user_
for each statement
execute procedure refresh_user();
drop view user_view cascade;
create view user_view as
select
u.id,
u.actor_id,
u.name,
u.avatar,
u.email,
u.matrix_user_id,
u.bio,
u.local,
u.admin,
u.banned,
u.show_avatars,
u.send_notifications_to_email,
u.published,
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
from user_ u;
create materialized view user_mview as select * from user_view;
create unique index idx_user_mview_id on user_mview (id);
-- community
drop trigger refresh_community on community;
create trigger refresh_community
after insert or update or delete or truncate
on community
for each statement
execute procedure refresh_community();
create or replace function refresh_community()
returns trigger language plpgsql
as $$
begin
refresh materialized view concurrently post_aggregates_mview;
refresh materialized view concurrently community_aggregates_mview;
refresh materialized view concurrently user_mview;
return null;
end $$;
drop view community_aggregates_view cascade;
create view community_aggregates_view as
-- Now that there's public and private keys, you have to be explicit here
select c.id,
c.name,
c.title,
c.description,
c.category_id,
c.creator_id,
c.removed,
c.published,
c.updated,
c.deleted,
c.nsfw,
c.actor_id,
c.local,
c.last_refreshed_at,
(select actor_id from user_ u where c.creator_id = u.id) as creator_actor_id,
(select local from user_ u where c.creator_id = u.id) as creator_local,
(select name from user_ u where c.creator_id = u.id) as creator_name,
(select avatar from user_ u where c.creator_id = u.id) as creator_avatar,
(select name from category ct where c.category_id = ct.id) as category_name,
(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
(select count(*) from post p where p.community_id = c.id) as number_of_posts,
(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments,
hot_rank((select count(*) from community_follower cf where cf.community_id = c.id), c.published) as hot_rank
from community c;
create materialized view community_aggregates_mview as select * from community_aggregates_view;
create unique index idx_community_aggregates_mview_id on community_aggregates_mview (id);
create view community_view as
with all_community as
(
select
ca.*
from community_aggregates_view ca
)
select
ac.*,
u.id as user_id,
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
from user_ u
cross join all_community ac
union all
select
ac.*,
null as user_id,
null as subscribed
from all_community ac
;
create view community_mview as
with all_community as
(
select
ca.*
from community_aggregates_mview ca
)
select
ac.*,
u.id as user_id,
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
from user_ u
cross join all_community ac
union all
select
ac.*,
null as user_id,
null as subscribed
from all_community ac
;
-- Post
drop view post_view;
drop view post_aggregates_view;
-- regen post view
create view post_aggregates_view as
select
p.*,
(select u.banned from user_ u where p.creator_id = u.id) as banned,
(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
(select actor_id from user_ where p.creator_id = user_.id) as creator_actor_id,
(select local from user_ where p.creator_id = user_.id) as creator_local,
(select name from user_ where p.creator_id = user_.id) as creator_name,
(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
(select actor_id from community where p.community_id = community.id) as community_actor_id,
(select local from community where p.community_id = community.id) as community_local,
(select name from community where p.community_id = community.id) as community_name,
(select removed from community c where p.community_id = c.id) as community_removed,
(select deleted from community c where p.community_id = c.id) as community_deleted,
(select nsfw from community c where p.community_id = c.id) as community_nsfw,
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
coalesce(sum(pl.score), 0) as score,
count (case when pl.score = 1 then 1 else null end) as upvotes,
count (case when pl.score = -1 then 1 else null end) as downvotes,
hot_rank(coalesce(sum(pl.score) , 0),
(
case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
else greatest(c.recent_comment_time, p.published)
end
)
) as hot_rank,
(
case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
else greatest(c.recent_comment_time, p.published)
end
) as newest_activity_time
from post p
left join post_like pl on p.id = pl.post_id
left join (
select post_id,
max(published) as recent_comment_time
from comment
group by 1
) c on p.id = c.post_id
group by p.id, c.recent_comment_time;
create materialized view post_aggregates_mview as select * from post_aggregates_view;
create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
create view post_view as
with all_post as (
select
pa.*
from post_aggregates_view pa
)
select
ap.*,
u.id as user_id,
coalesce(pl.score, 0) as my_vote,
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
from user_ u
cross join all_post ap
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
union all
select
ap.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from all_post ap
;
create view post_mview as
with all_post as (
select
pa.*
from post_aggregates_mview pa
)
select
ap.*,
u.id as user_id,
coalesce(pl.score, 0) as my_vote,
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
from user_ u
cross join all_post ap
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
union all
select
ap.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from all_post ap
;
drop trigger refresh_post on post;
create trigger refresh_post
after insert or update or delete or truncate
on post
for each statement
execute procedure refresh_post();
create or replace function refresh_post()
returns trigger language plpgsql
as $$
begin
refresh materialized view concurrently post_aggregates_mview;
refresh materialized view concurrently user_mview;
return null;
end $$;
-- User mention, comment, reply
drop view user_mention_view;
drop view comment_view;
drop view comment_aggregates_view;
-- reply and comment view
create view comment_aggregates_view as
select
c.*,
(select community_id from post p where p.id = c.post_id),
(select co.actor_id from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_actor_id,
(select co.local from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_local,
(select co.name from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_name,
(select u.banned from user_ u where c.creator_id = u.id) as banned,
(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
(select actor_id from user_ where c.creator_id = user_.id) as creator_actor_id,
(select local from user_ where c.creator_id = user_.id) as creator_local,
(select name from user_ where c.creator_id = user_.id) as creator_name,
(select avatar from user_ where c.creator_id = user_.id) as creator_avatar,
coalesce(sum(cl.score), 0) as score,
count (case when cl.score = 1 then 1 else null end) as upvotes,
count (case when cl.score = -1 then 1 else null end) as downvotes,
hot_rank(coalesce(sum(cl.score) , 0), c.published) as hot_rank
from comment c
left join comment_like cl on c.id = cl.comment_id
group by c.id;
create materialized view comment_aggregates_mview as select * from comment_aggregates_view;
create unique index idx_comment_aggregates_mview_id on comment_aggregates_mview (id);
create view comment_view as
with all_comment as
(
select
ca.*
from comment_aggregates_view ca
)
select
ac.*,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
from user_ u
cross join all_comment ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
union all
select
ac.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from all_comment ac
;
create view comment_mview as
with all_comment as
(
select
ca.*
from comment_aggregates_mview ca
)
select
ac.*,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
from user_ u
cross join all_comment ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
union all
select
ac.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from all_comment ac
;
-- Do the reply_view referencing the comment_mview
create view reply_view as
with closereply as (
select
c2.id,
c2.creator_id as sender_id,
c.creator_id as recipient_id
from comment c
inner join comment c2 on c.id = c2.parent_id
where c2.creator_id != c.creator_id
-- Do union where post is null
union
select
c.id,
c.creator_id as sender_id,
p.creator_id as recipient_id
from comment c, post p
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
)
select cv.*,
closereply.recipient_id
from comment_mview cv, closereply
where closereply.id = cv.id
;
-- user mention
create view user_mention_view as
select
c.id,
um.id as user_mention_id,
c.creator_id,
c.creator_actor_id,
c.creator_local,
c.post_id,
c.parent_id,
c.content,
c.removed,
um.read,
c.published,
c.updated,
c.deleted,
c.community_id,
c.community_actor_id,
c.community_local,
c.community_name,
c.banned,
c.banned_from_community,
c.creator_name,
c.creator_avatar,
c.score,
c.upvotes,
c.downvotes,
c.hot_rank,
c.user_id,
c.my_vote,
c.saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_mention um, comment_view c
where um.comment_id = c.id;
create view user_mention_mview as
with all_comment as
(
select
ca.*
from comment_aggregates_mview ca
)
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_ u
cross join all_comment ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
left join user_mention um on um.comment_id = ac.id
union all
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
null as user_id,
null as my_vote,
null as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from all_comment ac
left join user_mention um on um.comment_id = ac.id
;

View file

@ -0,0 +1,939 @@
-- Drop the mviews
drop view post_mview;
drop materialized view user_mview;
drop view community_mview;
drop materialized view private_message_mview;
drop view user_mention_mview;
drop view reply_view;
drop view comment_mview;
drop materialized view post_aggregates_mview;
drop materialized view community_aggregates_mview;
drop materialized view comment_aggregates_mview;
drop trigger refresh_private_message on private_message;
-- User
drop view user_view;
create view user_view as
select
u.id,
u.actor_id,
u.name,
u.avatar,
u.email,
u.matrix_user_id,
u.bio,
u.local,
u.admin,
u.banned,
u.show_avatars,
u.send_notifications_to_email,
u.published,
coalesce(pd.posts, 0) as number_of_posts,
coalesce(pd.score, 0) as post_score,
coalesce(cd.comments, 0) as number_of_comments,
coalesce(cd.score, 0) as comment_score
from user_ u
left join (
select
p.creator_id as creator_id,
count(distinct p.id) as posts,
sum(pl.score) as score
from post p
join post_like pl on p.id = pl.post_id
group by p.creator_id
) pd on u.id = pd.creator_id
left join (
select
c.creator_id,
count(distinct c.id) as comments,
sum(cl.score) as score
from comment c
join comment_like cl on c.id = cl.comment_id
group by c.creator_id
) cd on u.id = cd.creator_id;
create table user_fast as select * from user_view;
alter table user_fast add primary key (id);
drop trigger refresh_user on user_;
create trigger refresh_user
after insert or update or delete
on user_
for each row
execute procedure refresh_user();
-- Sample insert
-- insert into user_(name, password_encrypted) values ('test_name', 'bleh');
-- Sample delete
-- delete from user_ where name like 'test_name';
-- Sample update
-- update user_ set avatar = 'hai' where name like 'test_name';
create or replace function refresh_user()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'DELETE') THEN
delete from user_fast where id = OLD.id;
ELSIF (TG_OP = 'UPDATE') THEN
delete from user_fast where id = OLD.id;
insert into user_fast select * from user_view where id = NEW.id;
-- Refresh post_fast, cause of user info changes
delete from post_aggregates_fast where creator_id = NEW.id;
insert into post_aggregates_fast select * from post_aggregates_view where creator_id = NEW.id;
delete from comment_aggregates_fast where creator_id = NEW.id;
insert into comment_aggregates_fast select * from comment_aggregates_view where creator_id = NEW.id;
ELSIF (TG_OP = 'INSERT') THEN
insert into user_fast select * from user_view where id = NEW.id;
END IF;
return null;
end $$;
-- Post
-- Redoing the views : Credit eiknat
drop view post_view;
drop view post_aggregates_view;
create view post_aggregates_view as
select
p.*,
-- creator details
u.actor_id as creator_actor_id,
u."local" as creator_local,
u."name" as creator_name,
u.avatar as creator_avatar,
u.banned as banned,
cb.id::bool as banned_from_community,
-- community details
c.actor_id as community_actor_id,
c."local" as community_local,
c."name" as community_name,
c.removed as community_removed,
c.deleted as community_deleted,
c.nsfw as community_nsfw,
-- post score data/comment count
coalesce(ct.comments, 0) as number_of_comments,
coalesce(pl.score, 0) as score,
coalesce(pl.upvotes, 0) as upvotes,
coalesce(pl.downvotes, 0) as downvotes,
hot_rank(
coalesce(pl.score , 0), (
case
when (p.published < ('now'::timestamp - '1 month'::interval))
then p.published
else greatest(ct.recent_comment_time, p.published)
end
)
) as hot_rank,
(
case
when (p.published < ('now'::timestamp - '1 month'::interval))
then p.published
else greatest(ct.recent_comment_time, p.published)
end
) as newest_activity_time
from post p
left join user_ u on p.creator_id = u.id
left join community_user_ban cb on p.creator_id = cb.user_id and p.community_id = cb.community_id
left join community c on p.community_id = c.id
left join (
select
post_id,
count(*) as comments,
max(published) as recent_comment_time
from comment
group by post_id
) ct on ct.post_id = p.id
left join (
select
post_id,
sum(score) as score,
sum(score) filter (where score = 1) as upvotes,
-sum(score) filter (where score = -1) as downvotes
from post_like
group by post_id
) pl on pl.post_id = p.id
order by p.id;
create view post_view as
select
pav.*,
us.id as user_id,
us.user_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_read::bool as read,
us.is_saved::bool as saved
from post_aggregates_view pav
cross join lateral (
select
u.id,
coalesce(cf.community_id, 0) as is_subbed,
coalesce(pr.post_id, 0) as is_read,
coalesce(ps.post_id, 0) as is_saved,
coalesce(pl.score, 0) as user_vote
from user_ u
left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
) as us
union all
select
pav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from post_aggregates_view pav;
-- The post fast table
create table post_aggregates_fast as select * from post_aggregates_view;
alter table post_aggregates_fast add primary key (id);
-- For the hot rank resorting
create index idx_post_aggregates_fast_hot_rank_published on post_aggregates_fast (hot_rank desc, published desc);
create view post_fast_view as
select
pav.*,
us.id as user_id,
us.user_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_read::bool as read,
us.is_saved::bool as saved
from post_aggregates_fast pav
cross join lateral (
select
u.id,
coalesce(cf.community_id, 0) as is_subbed,
coalesce(pr.post_id, 0) as is_read,
coalesce(ps.post_id, 0) as is_saved,
coalesce(pl.score, 0) as user_vote
from user_ u
left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
) as us
union all
select
pav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from post_aggregates_fast pav;
drop trigger refresh_post on post;
create trigger refresh_post
after insert or update or delete
on post
for each row
execute procedure refresh_post();
-- Sample select
-- select id, name from post_fast_view where name like 'test_post' and user_id is null;
-- Sample insert
-- insert into post(name, creator_id, community_id) values ('test_post', 2, 2);
-- Sample delete
-- delete from post where name like 'test_post';
-- Sample update
-- update post set community_id = 4 where name like 'test_post';
create or replace function refresh_post()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'DELETE') THEN
delete from post_aggregates_fast where id = OLD.id;
-- Update community number of posts
update community_aggregates_fast set number_of_posts = number_of_posts - 1 where id = OLD.community_id;
ELSIF (TG_OP = 'UPDATE') THEN
delete from post_aggregates_fast where id = OLD.id;
insert into post_aggregates_fast select * from post_aggregates_view where id = NEW.id;
ELSIF (TG_OP = 'INSERT') THEN
insert into post_aggregates_fast select * from post_aggregates_view where id = NEW.id;
-- Update that users number of posts, post score
delete from user_fast where id = NEW.creator_id;
insert into user_fast select * from user_view where id = NEW.creator_id;
-- Update community number of posts
update community_aggregates_fast set number_of_posts = number_of_posts + 1 where id = NEW.community_id;
-- Update the hot rank on the post table
-- TODO this might not correctly update it, using a 1 week interval
update post_aggregates_fast as paf
set hot_rank = pav.hot_rank
from post_aggregates_view as pav
where paf.id = pav.id and (pav.published > ('now'::timestamp - '1 week'::interval));
END IF;
return null;
end $$;
-- Community
-- Redoing the views : Credit eiknat
drop view community_moderator_view;
drop view community_follower_view;
drop view community_user_ban_view;
drop view community_view;
drop view community_aggregates_view;
create view community_aggregates_view as
select
c.id,
c.name,
c.title,
c.description,
c.category_id,
c.creator_id,
c.removed,
c.published,
c.updated,
c.deleted,
c.nsfw,
c.actor_id,
c.local,
c.last_refreshed_at,
u.actor_id as creator_actor_id,
u.local as creator_local,
u.name as creator_name,
u.avatar as creator_avatar,
cat.name as category_name,
coalesce(cf.subs, 0) as number_of_subscribers,
coalesce(cd.posts, 0) as number_of_posts,
coalesce(cd.comments, 0) as number_of_comments,
hot_rank(cf.subs, c.published) as hot_rank
from community c
left join user_ u on c.creator_id = u.id
left join category cat on c.category_id = cat.id
left join (
select
p.community_id,
count(distinct p.id) as posts,
count(distinct ct.id) as comments
from post p
join comment ct on p.id = ct.post_id
group by p.community_id
) cd on cd.community_id = c.id
left join (
select
community_id,
count(*) as subs
from community_follower
group by community_id
) cf on cf.community_id = c.id;
create view community_view as
select
cv.*,
us.user as user_id,
us.is_subbed::bool as subscribed
from community_aggregates_view cv
cross join lateral (
select
u.id as user,
coalesce(cf.community_id, 0) as is_subbed
from user_ u
left join community_follower cf on u.id = cf.user_id and cf.community_id = cv.id
) as us
union all
select
cv.*,
null as user_id,
null as subscribed
from community_aggregates_view cv;
create view community_moderator_view as
select
cm.*,
u.actor_id as user_actor_id,
u.local as user_local,
u.name as user_name,
u.avatar as avatar,
c.actor_id as community_actor_id,
c.local as community_local,
c.name as community_name
from community_moderator cm
left join user_ u on cm.user_id = u.id
left join community c on cm.community_id = c.id;
create view community_follower_view as
select
cf.*,
u.actor_id as user_actor_id,
u.local as user_local,
u.name as user_name,
u.avatar as avatar,
c.actor_id as community_actor_id,
c.local as community_local,
c.name as community_name
from community_follower cf
left join user_ u on cf.user_id = u.id
left join community c on cf.community_id = c.id;
create view community_user_ban_view as
select
cb.*,
u.actor_id as user_actor_id,
u.local as user_local,
u.name as user_name,
u.avatar as avatar,
c.actor_id as community_actor_id,
c.local as community_local,
c.name as community_name
from community_user_ban cb
left join user_ u on cb.user_id = u.id
left join community c on cb.community_id = c.id;
-- The community fast table
create table community_aggregates_fast as select * from community_aggregates_view;
alter table community_aggregates_fast add primary key (id);
create view community_fast_view as
select
ac.*,
u.id as user_id,
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
from user_ u
cross join (
select
ca.*
from community_aggregates_fast ca
) ac
union all
select
caf.*,
null as user_id,
null as subscribed
from community_aggregates_fast caf;
drop trigger refresh_community on community;
create trigger refresh_community
after insert or update or delete
on community
for each row
execute procedure refresh_community();
-- Sample select
-- select * from community_fast_view where name like 'test_community_name' and user_id is null;
-- Sample insert
-- insert into community(name, title, category_id, creator_id) values ('test_community_name', 'test_community_title', 1, 2);
-- Sample delete
-- delete from community where name like 'test_community_name';
-- Sample update
-- update community set title = 'test_community_title_2' where name like 'test_community_name';
create or replace function refresh_community()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'DELETE') THEN
delete from community_aggregates_fast where id = OLD.id;
ELSIF (TG_OP = 'UPDATE') THEN
delete from community_aggregates_fast where id = OLD.id;
insert into community_aggregates_fast select * from community_aggregates_view where id = NEW.id;
-- Update user view due to owner changes
delete from user_fast where id = NEW.creator_id;
insert into user_fast select * from user_view where id = NEW.creator_id;
-- Update post view due to community changes
delete from post_aggregates_fast where community_id = NEW.id;
insert into post_aggregates_fast select * from post_aggregates_view where community_id = NEW.id;
-- TODO make sure this shows up in the users page ?
ELSIF (TG_OP = 'INSERT') THEN
insert into community_aggregates_fast select * from community_aggregates_view where id = NEW.id;
END IF;
return null;
end $$;
-- Comment
drop view user_mention_view;
drop view comment_view;
drop view comment_aggregates_view;
create view comment_aggregates_view as
select
ct.*,
-- community details
p.community_id,
c.actor_id as community_actor_id,
c."local" as community_local,
c."name" as community_name,
-- creator details
u.banned as banned,
coalesce(cb.id, 0)::bool as banned_from_community,
u.actor_id as creator_actor_id,
u.local as creator_local,
u.name as creator_name,
u.avatar as creator_avatar,
-- score details
coalesce(cl.total, 0) as score,
coalesce(cl.up, 0) as upvotes,
coalesce(cl.down, 0) as downvotes,
hot_rank(coalesce(cl.total, 0), ct.published) as hot_rank
from comment ct
left join post p on ct.post_id = p.id
left join community c on p.community_id = c.id
left join user_ u on ct.creator_id = u.id
left join community_user_ban cb on ct.creator_id = cb.user_id and p.id = ct.post_id and p.community_id = cb.community_id
left join (
select
l.comment_id as id,
sum(l.score) as total,
count(case when l.score = 1 then 1 else null end) as up,
count(case when l.score = -1 then 1 else null end) as down
from comment_like l
group by comment_id
) as cl on cl.id = ct.id;
create or replace view comment_view as (
select
cav.*,
us.user_id as user_id,
us.my_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_saved::bool as saved
from comment_aggregates_view cav
cross join lateral (
select
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
coalesce(cf.id, 0) as is_subbed,
coalesce(cs.id, 0) as is_saved
from user_ u
left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
) as us
union all
select
cav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from comment_aggregates_view cav
);
-- The fast view
create table comment_aggregates_fast as select * from comment_aggregates_view;
alter table comment_aggregates_fast add primary key (id);
create view comment_fast_view as
select
cav.*,
us.user_id as user_id,
us.my_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_saved::bool as saved
from comment_aggregates_fast cav
cross join lateral (
select
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
coalesce(cf.id, 0) as is_subbed,
coalesce(cs.id, 0) as is_saved
from user_ u
left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
) as us
union all
select
cav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from comment_aggregates_fast cav;
-- Do the reply_view referencing the comment_fast_view
create view reply_fast_view as
with closereply as (
select
c2.id,
c2.creator_id as sender_id,
c.creator_id as recipient_id
from comment c
inner join comment c2 on c.id = c2.parent_id
where c2.creator_id != c.creator_id
-- Do union where post is null
union
select
c.id,
c.creator_id as sender_id,
p.creator_id as recipient_id
from comment c, post p
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
)
select cv.*,
closereply.recipient_id
from comment_fast_view cv, closereply
where closereply.id = cv.id
;
-- user mention
create view user_mention_view as
select
c.id,
um.id as user_mention_id,
c.creator_id,
c.creator_actor_id,
c.creator_local,
c.post_id,
c.parent_id,
c.content,
c.removed,
um.read,
c.published,
c.updated,
c.deleted,
c.community_id,
c.community_actor_id,
c.community_local,
c.community_name,
c.banned,
c.banned_from_community,
c.creator_name,
c.creator_avatar,
c.score,
c.upvotes,
c.downvotes,
c.hot_rank,
c.user_id,
c.my_vote,
c.saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_mention um, comment_view c
where um.comment_id = c.id;
create view user_mention_fast_view as
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_ u
cross join (
select
ca.*
from comment_aggregates_fast ca
) ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
left join user_mention um on um.comment_id = ac.id
union all
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
null as user_id,
null as my_vote,
null as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from comment_aggregates_fast ac
left join user_mention um on um.comment_id = ac.id
;
drop trigger refresh_comment on comment;
create trigger refresh_comment
after insert or update or delete
on comment
for each row
execute procedure refresh_comment();
-- Sample select
-- select * from comment_fast_view where content = 'test_comment' and user_id is null;
-- Sample insert
-- insert into comment(creator_id, post_id, content) values (2, 2, 'test_comment');
-- Sample delete
-- delete from comment where content like 'test_comment';
-- Sample update
-- update comment set removed = true where content like 'test_comment';
create or replace function refresh_comment()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'DELETE') THEN
delete from comment_aggregates_fast where id = OLD.id;
-- Update community number of comments
update community_aggregates_fast as caf
set number_of_comments = number_of_comments - 1
from post as p
where caf.id = p.community_id and p.id = OLD.post_id;
ELSIF (TG_OP = 'UPDATE') THEN
delete from comment_aggregates_fast where id = OLD.id;
insert into comment_aggregates_fast select * from comment_aggregates_view where id = NEW.id;
ELSIF (TG_OP = 'INSERT') THEN
insert into comment_aggregates_fast select * from comment_aggregates_view where id = NEW.id;
-- Update user view due to comment count
update user_fast
set number_of_comments = number_of_comments + 1
where id = NEW.creator_id;
-- Update post view due to comment count, new comment activity time, but only on new posts
-- TODO this could be done more efficiently
delete from post_aggregates_fast where id = NEW.post_id;
insert into post_aggregates_fast select * from post_aggregates_view where id = NEW.post_id;
-- Force the hot rank as zero on week-older posts
update post_aggregates_fast as paf
set hot_rank = 0
where paf.id = NEW.post_id and (paf.published < ('now'::timestamp - '1 week'::interval));
-- Update community number of comments
update community_aggregates_fast as caf
set number_of_comments = number_of_comments + 1
from post as p
where caf.id = p.community_id and p.id = NEW.post_id;
END IF;
return null;
end $$;
-- post_like
-- select id, score, my_vote from post_fast_view where id = 29 and user_id = 4;
-- Sample insert
-- insert into post_like(user_id, post_id, score) values (4, 29, 1);
-- Sample delete
-- delete from post_like where user_id = 4 and post_id = 29;
-- Sample update
-- update post_like set score = -1 where user_id = 4 and post_id = 29;
-- TODO test this a LOT
create or replace function refresh_post_like()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'DELETE') THEN
update post_aggregates_fast
set score = case
when (OLD.score = 1) then score - 1
else score + 1 end,
upvotes = case
when (OLD.score = 1) then upvotes - 1
else upvotes end,
downvotes = case
when (OLD.score = -1) then downvotes - 1
else downvotes end
where id = OLD.post_id;
ELSIF (TG_OP = 'INSERT') THEN
update post_aggregates_fast
set score = case
when (NEW.score = 1) then score + 1
else score - 1 end,
upvotes = case
when (NEW.score = 1) then upvotes + 1
else upvotes end,
downvotes = case
when (NEW.score = -1) then downvotes + 1
else downvotes end
where id = NEW.post_id;
END IF;
return null;
end $$;
drop trigger refresh_post_like on post_like;
create trigger refresh_post_like
after insert or delete
on post_like
for each row
execute procedure refresh_post_like();
-- comment_like
-- select id, score, my_vote from comment_fast_view where id = 29 and user_id = 4;
-- Sample insert
-- insert into comment_like(user_id, comment_id, post_id, score) values (4, 29, 51, 1);
-- Sample delete
-- delete from comment_like where user_id = 4 and comment_id = 29;
-- Sample update
-- update comment_like set score = -1 where user_id = 4 and comment_id = 29;
create or replace function refresh_comment_like()
returns trigger language plpgsql
as $$
begin
-- TODO possibly select from comment_fast to get previous scores, instead of re-fetching the views?
IF (TG_OP = 'DELETE') THEN
update comment_aggregates_fast
set score = case
when (OLD.score = 1) then score - 1
else score + 1 end,
upvotes = case
when (OLD.score = 1) then upvotes - 1
else upvotes end,
downvotes = case
when (OLD.score = -1) then downvotes - 1
else downvotes end
where id = OLD.comment_id;
ELSIF (TG_OP = 'INSERT') THEN
update comment_aggregates_fast
set score = case
when (NEW.score = 1) then score + 1
else score - 1 end,
upvotes = case
when (NEW.score = 1) then upvotes + 1
else upvotes end,
downvotes = case
when (NEW.score = -1) then downvotes + 1
else downvotes end
where id = NEW.comment_id;
END IF;
return null;
end $$;
drop trigger refresh_comment_like on comment_like;
create trigger refresh_comment_like
after insert or delete
on comment_like
for each row
execute procedure refresh_comment_like();
-- Community user ban
drop trigger refresh_community_user_ban on community_user_ban;
create trigger refresh_community_user_ban
after insert or delete -- Note this is missing after update
on community_user_ban
for each row
execute procedure refresh_community_user_ban();
-- select creator_name, banned_from_community from comment_fast_view where user_id = 4 and content = 'test_before_ban';
-- select creator_name, banned_from_community, community_id from comment_aggregates_fast where content = 'test_before_ban';
-- Sample insert
-- insert into comment(creator_id, post_id, content) values (1198, 341, 'test_before_ban');
-- insert into community_user_ban(community_id, user_id) values (2, 1198);
-- Sample delete
-- delete from community_user_ban where user_id = 1198 and community_id = 2;
-- delete from comment where content = 'test_before_ban';
-- update comment_aggregates_fast set banned_from_community = false where creator_id = 1198 and community_id = 2;
create or replace function refresh_community_user_ban()
returns trigger language plpgsql
as $$
begin
-- TODO possibly select from comment_fast to get previous scores, instead of re-fetching the views?
IF (TG_OP = 'DELETE') THEN
update comment_aggregates_fast set banned_from_community = false where creator_id = OLD.user_id and community_id = OLD.community_id;
update post_aggregates_fast set banned_from_community = false where creator_id = OLD.user_id and community_id = OLD.community_id;
ELSIF (TG_OP = 'INSERT') THEN
update comment_aggregates_fast set banned_from_community = true where creator_id = NEW.user_id and community_id = NEW.community_id;
update post_aggregates_fast set banned_from_community = true where creator_id = NEW.user_id and community_id = NEW.community_id;
END IF;
return null;
end $$;
-- Community follower
drop trigger refresh_community_follower on community_follower;
create trigger refresh_community_follower
after insert or delete -- Note this is missing after update
on community_follower
for each row
execute procedure refresh_community_follower();
create or replace function refresh_community_follower()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'DELETE') THEN
update community_aggregates_fast set number_of_subscribers = number_of_subscribers - 1 where id = OLD.community_id;
ELSIF (TG_OP = 'INSERT') THEN
update community_aggregates_fast set number_of_subscribers = number_of_subscribers + 1 where id = NEW.community_id;
END IF;
return null;
end $$;

View file

@ -0,0 +1,388 @@
drop view user_mention_view;
drop view reply_fast_view;
drop view comment_fast_view;
drop view comment_view;
drop view user_mention_fast_view;
drop table comment_aggregates_fast;
drop view comment_aggregates_view;
create view comment_aggregates_view as
select
ct.*,
-- community details
p.community_id,
c.actor_id as community_actor_id,
c."local" as community_local,
c."name" as community_name,
-- creator details
u.banned as banned,
coalesce(cb.id, 0)::bool as banned_from_community,
u.actor_id as creator_actor_id,
u.local as creator_local,
u.name as creator_name,
u.avatar as creator_avatar,
-- score details
coalesce(cl.total, 0) as score,
coalesce(cl.up, 0) as upvotes,
coalesce(cl.down, 0) as downvotes,
hot_rank(coalesce(cl.total, 0), ct.published) as hot_rank
from comment ct
left join post p on ct.post_id = p.id
left join community c on p.community_id = c.id
left join user_ u on ct.creator_id = u.id
left join community_user_ban cb on ct.creator_id = cb.user_id and p.id = ct.post_id and p.community_id = cb.community_id
left join (
select
l.comment_id as id,
sum(l.score) as total,
count(case when l.score = 1 then 1 else null end) as up,
count(case when l.score = -1 then 1 else null end) as down
from comment_like l
group by comment_id
) as cl on cl.id = ct.id;
create or replace view comment_view as (
select
cav.*,
us.user_id as user_id,
us.my_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_saved::bool as saved
from comment_aggregates_view cav
cross join lateral (
select
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
coalesce(cf.id, 0) as is_subbed,
coalesce(cs.id, 0) as is_saved
from user_ u
left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
) as us
union all
select
cav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from comment_aggregates_view cav
);
create table comment_aggregates_fast as select * from comment_aggregates_view;
alter table comment_aggregates_fast add primary key (id);
create view comment_fast_view as
select
cav.*,
us.user_id as user_id,
us.my_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_saved::bool as saved
from comment_aggregates_fast cav
cross join lateral (
select
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
coalesce(cf.id, 0) as is_subbed,
coalesce(cs.id, 0) as is_saved
from user_ u
left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
) as us
union all
select
cav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from comment_aggregates_fast cav;
create view user_mention_view as
select
c.id,
um.id as user_mention_id,
c.creator_id,
c.creator_actor_id,
c.creator_local,
c.post_id,
c.parent_id,
c.content,
c.removed,
um.read,
c.published,
c.updated,
c.deleted,
c.community_id,
c.community_actor_id,
c.community_local,
c.community_name,
c.banned,
c.banned_from_community,
c.creator_name,
c.creator_avatar,
c.score,
c.upvotes,
c.downvotes,
c.hot_rank,
c.user_id,
c.my_vote,
c.saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_mention um, comment_view c
where um.comment_id = c.id;
create view user_mention_fast_view as
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_ u
cross join (
select
ca.*
from comment_aggregates_fast ca
) ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
left join user_mention um on um.comment_id = ac.id
union all
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
null as user_id,
null as my_vote,
null as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from comment_aggregates_fast ac
left join user_mention um on um.comment_id = ac.id
;
-- Do the reply_view referencing the comment_fast_view
create view reply_fast_view as
with closereply as (
select
c2.id,
c2.creator_id as sender_id,
c.creator_id as recipient_id
from comment c
inner join comment c2 on c.id = c2.parent_id
where c2.creator_id != c.creator_id
-- Do union where post is null
union
select
c.id,
c.creator_id as sender_id,
p.creator_id as recipient_id
from comment c, post p
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
)
select cv.*,
closereply.recipient_id
from comment_fast_view cv, closereply
where closereply.id = cv.id
;
-- add creator_published to the post view
drop view post_fast_view;
drop table post_aggregates_fast;
drop view post_view;
drop view post_aggregates_view;
create view post_aggregates_view as
select
p.*,
-- creator details
u.actor_id as creator_actor_id,
u."local" as creator_local,
u."name" as creator_name,
u.avatar as creator_avatar,
u.banned as banned,
cb.id::bool as banned_from_community,
-- community details
c.actor_id as community_actor_id,
c."local" as community_local,
c."name" as community_name,
c.removed as community_removed,
c.deleted as community_deleted,
c.nsfw as community_nsfw,
-- post score data/comment count
coalesce(ct.comments, 0) as number_of_comments,
coalesce(pl.score, 0) as score,
coalesce(pl.upvotes, 0) as upvotes,
coalesce(pl.downvotes, 0) as downvotes,
hot_rank(
coalesce(pl.score , 0), (
case
when (p.published < ('now'::timestamp - '1 month'::interval))
then p.published
else greatest(ct.recent_comment_time, p.published)
end
)
) as hot_rank,
(
case
when (p.published < ('now'::timestamp - '1 month'::interval))
then p.published
else greatest(ct.recent_comment_time, p.published)
end
) as newest_activity_time
from post p
left join user_ u on p.creator_id = u.id
left join community_user_ban cb on p.creator_id = cb.user_id and p.community_id = cb.community_id
left join community c on p.community_id = c.id
left join (
select
post_id,
count(*) as comments,
max(published) as recent_comment_time
from comment
group by post_id
) ct on ct.post_id = p.id
left join (
select
post_id,
sum(score) as score,
sum(score) filter (where score = 1) as upvotes,
-sum(score) filter (where score = -1) as downvotes
from post_like
group by post_id
) pl on pl.post_id = p.id
order by p.id;
create view post_view as
select
pav.*,
us.id as user_id,
us.user_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_read::bool as read,
us.is_saved::bool as saved
from post_aggregates_view pav
cross join lateral (
select
u.id,
coalesce(cf.community_id, 0) as is_subbed,
coalesce(pr.post_id, 0) as is_read,
coalesce(ps.post_id, 0) as is_saved,
coalesce(pl.score, 0) as user_vote
from user_ u
left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
) as us
union all
select
pav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from post_aggregates_view pav;
create table post_aggregates_fast as select * from post_aggregates_view;
alter table post_aggregates_fast add primary key (id);
create view post_fast_view as
select
pav.*,
us.id as user_id,
us.user_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_read::bool as read,
us.is_saved::bool as saved
from post_aggregates_fast pav
cross join lateral (
select
u.id,
coalesce(cf.community_id, 0) as is_subbed,
coalesce(pr.post_id, 0) as is_read,
coalesce(ps.post_id, 0) as is_saved,
coalesce(pl.score, 0) as user_vote
from user_ u
left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
) as us
union all
select
pav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from post_aggregates_fast pav;

View file

@ -0,0 +1,390 @@
drop view user_mention_view;
drop view reply_fast_view;
drop view comment_fast_view;
drop view comment_view;
drop view user_mention_fast_view;
drop table comment_aggregates_fast;
drop view comment_aggregates_view;
create view comment_aggregates_view as
select
ct.*,
-- community details
p.community_id,
c.actor_id as community_actor_id,
c."local" as community_local,
c."name" as community_name,
-- creator details
u.banned as banned,
coalesce(cb.id, 0)::bool as banned_from_community,
u.actor_id as creator_actor_id,
u.local as creator_local,
u.name as creator_name,
u.published as creator_published,
u.avatar as creator_avatar,
-- score details
coalesce(cl.total, 0) as score,
coalesce(cl.up, 0) as upvotes,
coalesce(cl.down, 0) as downvotes,
hot_rank(coalesce(cl.total, 0), ct.published) as hot_rank
from comment ct
left join post p on ct.post_id = p.id
left join community c on p.community_id = c.id
left join user_ u on ct.creator_id = u.id
left join community_user_ban cb on ct.creator_id = cb.user_id and p.id = ct.post_id and p.community_id = cb.community_id
left join (
select
l.comment_id as id,
sum(l.score) as total,
count(case when l.score = 1 then 1 else null end) as up,
count(case when l.score = -1 then 1 else null end) as down
from comment_like l
group by comment_id
) as cl on cl.id = ct.id;
create or replace view comment_view as (
select
cav.*,
us.user_id as user_id,
us.my_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_saved::bool as saved
from comment_aggregates_view cav
cross join lateral (
select
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
coalesce(cf.id, 0) as is_subbed,
coalesce(cs.id, 0) as is_saved
from user_ u
left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
) as us
union all
select
cav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from comment_aggregates_view cav
);
create table comment_aggregates_fast as select * from comment_aggregates_view;
alter table comment_aggregates_fast add primary key (id);
create view comment_fast_view as
select
cav.*,
us.user_id as user_id,
us.my_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_saved::bool as saved
from comment_aggregates_fast cav
cross join lateral (
select
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
coalesce(cf.id, 0) as is_subbed,
coalesce(cs.id, 0) as is_saved
from user_ u
left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
) as us
union all
select
cav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from comment_aggregates_fast cav;
create view user_mention_view as
select
c.id,
um.id as user_mention_id,
c.creator_id,
c.creator_actor_id,
c.creator_local,
c.post_id,
c.parent_id,
c.content,
c.removed,
um.read,
c.published,
c.updated,
c.deleted,
c.community_id,
c.community_actor_id,
c.community_local,
c.community_name,
c.banned,
c.banned_from_community,
c.creator_name,
c.creator_avatar,
c.score,
c.upvotes,
c.downvotes,
c.hot_rank,
c.user_id,
c.my_vote,
c.saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_mention um, comment_view c
where um.comment_id = c.id;
create view user_mention_fast_view as
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_ u
cross join (
select
ca.*
from comment_aggregates_fast ca
) ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
left join user_mention um on um.comment_id = ac.id
union all
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
null as user_id,
null as my_vote,
null as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from comment_aggregates_fast ac
left join user_mention um on um.comment_id = ac.id
;
-- Do the reply_view referencing the comment_fast_view
create view reply_fast_view as
with closereply as (
select
c2.id,
c2.creator_id as sender_id,
c.creator_id as recipient_id
from comment c
inner join comment c2 on c.id = c2.parent_id
where c2.creator_id != c.creator_id
-- Do union where post is null
union
select
c.id,
c.creator_id as sender_id,
p.creator_id as recipient_id
from comment c, post p
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
)
select cv.*,
closereply.recipient_id
from comment_fast_view cv, closereply
where closereply.id = cv.id
;
-- add creator_published to the post view
drop view post_fast_view;
drop table post_aggregates_fast;
drop view post_view;
drop view post_aggregates_view;
create view post_aggregates_view as
select
p.*,
-- creator details
u.actor_id as creator_actor_id,
u."local" as creator_local,
u."name" as creator_name,
u.published as creator_published,
u.avatar as creator_avatar,
u.banned as banned,
cb.id::bool as banned_from_community,
-- community details
c.actor_id as community_actor_id,
c."local" as community_local,
c."name" as community_name,
c.removed as community_removed,
c.deleted as community_deleted,
c.nsfw as community_nsfw,
-- post score data/comment count
coalesce(ct.comments, 0) as number_of_comments,
coalesce(pl.score, 0) as score,
coalesce(pl.upvotes, 0) as upvotes,
coalesce(pl.downvotes, 0) as downvotes,
hot_rank(
coalesce(pl.score , 0), (
case
when (p.published < ('now'::timestamp - '1 month'::interval))
then p.published
else greatest(ct.recent_comment_time, p.published)
end
)
) as hot_rank,
(
case
when (p.published < ('now'::timestamp - '1 month'::interval))
then p.published
else greatest(ct.recent_comment_time, p.published)
end
) as newest_activity_time
from post p
left join user_ u on p.creator_id = u.id
left join community_user_ban cb on p.creator_id = cb.user_id and p.community_id = cb.community_id
left join community c on p.community_id = c.id
left join (
select
post_id,
count(*) as comments,
max(published) as recent_comment_time
from comment
group by post_id
) ct on ct.post_id = p.id
left join (
select
post_id,
sum(score) as score,
sum(score) filter (where score = 1) as upvotes,
-sum(score) filter (where score = -1) as downvotes
from post_like
group by post_id
) pl on pl.post_id = p.id
order by p.id;
create view post_view as
select
pav.*,
us.id as user_id,
us.user_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_read::bool as read,
us.is_saved::bool as saved
from post_aggregates_view pav
cross join lateral (
select
u.id,
coalesce(cf.community_id, 0) as is_subbed,
coalesce(pr.post_id, 0) as is_read,
coalesce(ps.post_id, 0) as is_saved,
coalesce(pl.score, 0) as user_vote
from user_ u
left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
) as us
union all
select
pav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from post_aggregates_view pav;
create table post_aggregates_fast as select * from post_aggregates_view;
alter table post_aggregates_fast add primary key (id);
create view post_fast_view as
select
pav.*,
us.id as user_id,
us.user_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_read::bool as read,
us.is_saved::bool as saved
from post_aggregates_fast pav
cross join lateral (
select
u.id,
coalesce(cf.community_id, 0) as is_subbed,
coalesce(pr.post_id, 0) as is_read,
coalesce(ps.post_id, 0) as is_saved,
coalesce(pl.score, 0) as user_vote
from user_ u
left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
) as us
union all
select
pav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from post_aggregates_fast pav;

View file

@ -1,31 +1,42 @@
#!/bin/bash #!/bin/bash
set -e set -e
# You can import these to http://tatiyants.com/pev/#/plans/new
# Do the views first # Do the views first
echo "explain (analyze, format json) select * from user_mview" > explain.sql echo "explain (analyze, format json) select * from user_fast" > explain.sql
psql -qAt -U lemmy -f explain.sql > user_view.json psql -qAt -U lemmy -f explain.sql > user_fast.json
echo "explain (analyze, format json) select * from post_mview where user_id is null order by hot_rank desc, published desc" > explain.sql echo "explain (analyze, format json) select * from post_view where user_id is null order by hot_rank desc, published desc" > explain.sql
psql -qAt -U lemmy -f explain.sql > post_view.json psql -qAt -U lemmy -f explain.sql > post_view.json
echo "explain (analyze, format json) select * from comment_mview where user_id is null" > explain.sql echo "explain (analyze, format json) select * from post_fast_view where user_id is null order by hot_rank desc, published desc" > explain.sql
psql -qAt -U lemmy -f explain.sql > post_fast_view.json
echo "explain (analyze, format json) select * from comment_view where user_id is null" > explain.sql
psql -qAt -U lemmy -f explain.sql > comment_view.json psql -qAt -U lemmy -f explain.sql > comment_view.json
echo "explain (analyze, format json) select * from community_mview where user_id is null order by hot_rank desc" > explain.sql echo "explain (analyze, format json) select * from comment_fast_view where user_id is null" > explain.sql
psql -qAt -U lemmy -f explain.sql > comment_fast_view.json
echo "explain (analyze, format json) select * from community_view where user_id is null order by hot_rank desc" > explain.sql
psql -qAt -U lemmy -f explain.sql > community_view.json psql -qAt -U lemmy -f explain.sql > community_view.json
echo "explain (analyze, format json) select * from community_fast_view where user_id is null order by hot_rank desc" > explain.sql
psql -qAt -U lemmy -f explain.sql > community_fast_view.json
echo "explain (analyze, format json) select * from site_view limit 1" > explain.sql echo "explain (analyze, format json) select * from site_view limit 1" > explain.sql
psql -qAt -U lemmy -f explain.sql > site_view.json psql -qAt -U lemmy -f explain.sql > site_view.json
echo "explain (analyze, format json) select * from reply_view where user_id = 34 and recipient_id = 34" > explain.sql echo "explain (analyze, format json) select * from reply_fast_view where user_id = 34 and recipient_id = 34" > explain.sql
psql -qAt -U lemmy -f explain.sql > reply_view.json psql -qAt -U lemmy -f explain.sql > reply_fast_view.json
echo "explain (analyze, format json) select * from user_mention_view where user_id = 34 and recipient_id = 34" > explain.sql echo "explain (analyze, format json) select * from user_mention_view where user_id = 34 and recipient_id = 34" > explain.sql
psql -qAt -U lemmy -f explain.sql > user_mention_view.json psql -qAt -U lemmy -f explain.sql > user_mention_view.json
echo "explain (analyze, format json) select * from user_mention_mview where user_id = 34 and recipient_id = 34" > explain.sql echo "explain (analyze, format json) select * from user_mention_fast_view where user_id = 34 and recipient_id = 34" > explain.sql
psql -qAt -U lemmy -f explain.sql > user_mention_mview.json psql -qAt -U lemmy -f explain.sql > user_mention_fast_view.json
grep "Execution Time" *.json grep "Execution Time" *.json

73
server/src/api/claims.rs Normal file
View file

@ -0,0 +1,73 @@
use diesel::{result::Error, PgConnection};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
use lemmy_db::{user::User_, Crud};
use lemmy_utils::{is_email_regex, settings::Settings};
use serde::{Deserialize, Serialize};
type Jwt = String;
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub id: i32,
pub username: String,
pub iss: String,
pub show_nsfw: bool,
pub theme: String,
pub default_sort_type: i16,
pub default_listing_type: i16,
pub lang: String,
pub avatar: Option<String>,
pub show_avatars: bool,
}
impl Claims {
pub fn decode(jwt: &str) -> Result<TokenData<Claims>, jsonwebtoken::errors::Error> {
let v = Validation {
validate_exp: false,
..Validation::default()
};
decode::<Claims>(
&jwt,
&DecodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
&v,
)
}
pub fn jwt(user: User_, hostname: String) -> Jwt {
let my_claims = Claims {
id: user.id,
username: user.name.to_owned(),
iss: hostname,
show_nsfw: user.show_nsfw,
theme: user.theme.to_owned(),
default_sort_type: user.default_sort_type,
default_listing_type: user.default_listing_type,
lang: user.lang.to_owned(),
avatar: user.avatar.to_owned(),
show_avatars: user.show_avatars.to_owned(),
};
encode(
&Header::default(),
&my_claims,
&EncodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
)
.unwrap()
}
// TODO: move these into user?
pub fn find_by_email_or_username(
conn: &PgConnection,
username_or_email: &str,
) -> Result<User_, Error> {
if is_email_regex(username_or_email) {
User_::find_by_email(conn, username_or_email)
} else {
User_::find_by_username(conn, username_or_email)
}
}
pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<User_, Error> {
let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims;
User_::read(&conn, claims.id)
}
}

View file

@ -1,28 +1,7 @@
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{claims::Claims, APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType}, apub::{ApubLikeableType, ApubObjectType},
blocking, blocking,
db::{
comment::*,
comment_view::*,
community_view::*,
moderator::*,
post::*,
site_view::*,
user::*,
user_mention::*,
user_view::*,
Crud,
Likeable,
ListingType,
Saveable,
SortType,
},
naive_now,
remove_slurs,
scrape_text_for_mentions,
send_email,
settings::Settings,
websocket::{ websocket::{
server::{JoinCommunityRoom, SendComment}, server::{JoinCommunityRoom, SendComment},
UserOperation, UserOperation,
@ -30,6 +9,31 @@ use crate::{
}, },
DbPool, DbPool,
LemmyError, LemmyError,
};
use lemmy_db::{
comment::*,
comment_view::*,
community_view::*,
moderator::*,
naive_now,
post::*,
site_view::*,
user::*,
user_mention::*,
user_view::*,
Crud,
Likeable,
ListingType,
Saveable,
SortType,
};
use lemmy_utils::{
make_apub_endpoint,
remove_slurs,
scrape_text_for_mentions,
send_email,
settings::Settings,
EndpointType,
MentionData, MentionData,
}; };
use log::error; use log::error;
@ -155,7 +159,9 @@ impl Perform for Oper<CreateComment> {
let inserted_comment_id = inserted_comment.id; let inserted_comment_id = inserted_comment.id;
let updated_comment: Comment = match blocking(pool, move |conn| { let updated_comment: Comment = match blocking(pool, move |conn| {
Comment::update_ap_id(&conn, inserted_comment_id) let apub_id =
make_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string()).to_string();
Comment::update_ap_id(&conn, inserted_comment_id, apub_id)
}) })
.await? .await?
{ {

View file

@ -1,26 +1,24 @@
use super::*; use super::*;
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{claims::Claims, APIError, Oper, Perform},
apub::{ apub::ActorType,
extensions::signatures::generate_actor_keypair,
make_apub_endpoint,
ActorType,
EndpointType,
},
blocking, blocking,
db::{Bannable, Crud, Followable, Joinable, SortType},
is_valid_community_name,
naive_from_unix,
naive_now,
slur_check,
slurs_vec_to_str,
websocket::{ websocket::{
server::{JoinCommunityRoom, SendCommunityRoomMessage}, server::{JoinCommunityRoom, SendCommunityRoomMessage},
UserOperation, UserOperation,
WebsocketInfo, WebsocketInfo,
}, },
DbPool, DbPool,
LemmyError, };
use lemmy_db::{naive_now, Bannable, Crud, Followable, Joinable, SortType};
use lemmy_utils::{
generate_actor_keypair,
is_valid_community_name,
make_apub_endpoint,
naive_from_unix,
slur_check,
slurs_vec_to_str,
EndpointType,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;

View file

@ -1,11 +1,8 @@
use crate::{ use crate::{websocket::WebsocketInfo, DbPool, LemmyError};
db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*},
websocket::WebsocketInfo,
DbPool,
LemmyError,
};
use actix_web::client::Client; use actix_web::client::Client;
use lemmy_db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*};
pub mod claims;
pub mod comment; pub mod comment;
pub mod community; pub mod community;
pub mod post; pub mod post;

View file

@ -1,27 +1,8 @@
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{claims::Claims, APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType}, apub::{ApubLikeableType, ApubObjectType},
blocking, blocking,
db::{
comment_view::*,
community_view::*,
moderator::*,
post::*,
post_view::*,
site::*,
site_view::*,
user::*,
user_view::*,
Crud,
Likeable,
ListingType,
Saveable,
SortType,
},
fetch_iframely_and_pictrs_data, fetch_iframely_and_pictrs_data,
naive_now,
slur_check,
slurs_vec_to_str,
websocket::{ websocket::{
server::{JoinCommunityRoom, JoinPostRoom, SendPost}, server::{JoinCommunityRoom, JoinPostRoom, SendPost},
UserOperation, UserOperation,
@ -30,6 +11,24 @@ use crate::{
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use lemmy_db::{
comment_view::*,
community_view::*,
moderator::*,
naive_now,
post::*,
post_view::*,
site::*,
site_view::*,
user::*,
user_view::*,
Crud,
Likeable,
ListingType,
Saveable,
SortType,
};
use lemmy_utils::{is_valid_post_title, make_apub_endpoint, slur_check, slurs_vec_to_str, EndpointType};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
@ -136,6 +135,10 @@ impl Perform for Oper<CreatePost> {
} }
} }
if !is_valid_post_title(&data.name) {
return Err(APIError::err("invalid_post_title").into());
}
let user_id = claims.id; let user_id = claims.id;
// Check for a community ban // Check for a community ban
@ -157,7 +160,7 @@ impl Perform for Oper<CreatePost> {
fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await; fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
let post_form = PostForm { let post_form = PostForm {
name: data.name.to_owned(), name: data.name.trim().to_owned(),
url: data.url.to_owned(), url: data.url.to_owned(),
body: data.body.to_owned(), body: data.body.to_owned(),
community_id: data.community_id, community_id: data.community_id,
@ -191,11 +194,16 @@ impl Perform for Oper<CreatePost> {
}; };
let inserted_post_id = inserted_post.id; let inserted_post_id = inserted_post.id;
let updated_post = let updated_post = match blocking(pool, move |conn| {
match blocking(pool, move |conn| Post::update_ap_id(conn, inserted_post_id)).await? { let apub_id =
Ok(post) => post, make_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string()).to_string();
Err(_e) => return Err(APIError::err("couldnt_create_post").into()), Post::update_ap_id(conn, inserted_post_id, apub_id)
}; })
.await?
{
Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
};
updated_post.send_create(&user, &self.client, pool).await?; updated_post.send_create(&user, &self.client, pool).await?;
@ -512,6 +520,10 @@ impl Perform for Oper<EditPost> {
} }
} }
if !is_valid_post_title(&data.name) {
return Err(APIError::err("invalid_post_title").into());
}
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()), Err(_e) => return Err(APIError::err("not_logged_in").into()),
@ -561,7 +573,7 @@ impl Perform for Oper<EditPost> {
let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??; let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
let post_form = PostForm { let post_form = PostForm {
name: data.name.to_owned(), name: data.name.trim().to_owned(),
url: data.url.to_owned(), url: data.url.to_owned(),
body: data.body.to_owned(), body: data.body.to_owned(),
creator_id: data.creator_id.to_owned(), creator_id: data.creator_id.to_owned(),

View file

@ -1,31 +1,28 @@
use super::user::Register; use super::user::Register;
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{claims::Claims, APIError, Oper, Perform},
apub::fetcher::search_by_apub_id, apub::fetcher::search_by_apub_id,
blocking, blocking,
db::{
category::*,
comment_view::*,
community_view::*,
moderator::*,
moderator_views::*,
post_view::*,
site::*,
site_view::*,
user::*,
user_view::*,
Crud,
SearchType,
SortType,
},
naive_now,
settings::Settings,
slur_check,
slurs_vec_to_str,
websocket::{server::SendAllMessage, UserOperation, WebsocketInfo}, websocket::{server::SendAllMessage, UserOperation, WebsocketInfo},
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use lemmy_db::{
category::*,
comment_view::*,
community_view::*,
moderator::*,
moderator_views::*,
naive_now,
post_view::*,
site::*,
site_view::*,
user_view::*,
Crud,
SearchType,
SortType,
};
use lemmy_utils::{settings::Settings, slur_check, slurs_vec_to_str};
use log::{debug, info}; use log::{debug, info};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;

View file

@ -1,44 +1,7 @@
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{claims::Claims, APIError, Oper, Perform},
apub::{ apub::ApubObjectType,
extensions::signatures::generate_actor_keypair,
make_apub_endpoint,
ApubObjectType,
EndpointType,
},
blocking, blocking,
db::{
comment::*,
comment_view::*,
community::*,
community_view::*,
moderator::*,
password_reset_request::*,
post::*,
post_view::*,
private_message::*,
private_message_view::*,
site::*,
site_view::*,
user::*,
user_mention::*,
user_mention_view::*,
user_view::*,
Crud,
Followable,
Joinable,
ListingType,
SortType,
},
generate_random_string,
is_valid_username,
naive_from_unix,
naive_now,
remove_slurs,
send_email,
settings::Settings,
slur_check,
slurs_vec_to_str,
websocket::{ websocket::{
server::{JoinUserRoom, SendAllMessage, SendUserRoomMessage}, server::{JoinUserRoom, SendAllMessage, SendUserRoomMessage},
UserOperation, UserOperation,
@ -48,6 +11,43 @@ use crate::{
LemmyError, LemmyError,
}; };
use bcrypt::verify; use bcrypt::verify;
use lemmy_db::{
comment::*,
comment_view::*,
community::*,
community_view::*,
moderator::*,
naive_now,
password_reset_request::*,
post::*,
post_view::*,
private_message::*,
private_message_view::*,
site::*,
site_view::*,
user::*,
user_mention::*,
user_mention_view::*,
user_view::*,
Crud,
Followable,
Joinable,
ListingType,
SortType,
};
use lemmy_utils::{
generate_actor_keypair,
generate_random_string,
is_valid_username,
make_apub_endpoint,
naive_from_unix,
remove_slurs,
send_email,
settings::Settings,
slur_check,
slurs_vec_to_str,
EndpointType,
};
use log::error; use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
@ -264,7 +264,7 @@ impl Perform for Oper<Login> {
// Fetch that username / email // Fetch that username / email
let username_or_email = data.username_or_email.clone(); let username_or_email = data.username_or_email.clone();
let user = match blocking(pool, move |conn| { let user = match blocking(pool, move |conn| {
User_::find_by_email_or_username(conn, &username_or_email) Claims::find_by_email_or_username(conn, &username_or_email)
}) })
.await? .await?
{ {
@ -279,7 +279,9 @@ impl Perform for Oper<Login> {
} }
// Return the jwt // Return the jwt
Ok(LoginResponse { jwt: user.jwt() }) Ok(LoginResponse {
jwt: Claims::jwt(user, Settings::get().hostname),
})
} }
} }
@ -421,7 +423,7 @@ impl Perform for Oper<Register> {
// Return the jwt // Return the jwt
Ok(LoginResponse { Ok(LoginResponse {
jwt: inserted_user.jwt(), jwt: Claims::jwt(inserted_user, Settings::get().hostname),
}) })
} }
} }
@ -451,6 +453,11 @@ impl Perform for Oper<SaveUserSettings> {
None => read_user.email, None => read_user.email,
}; };
let avatar = match &data.avatar {
Some(avatar) => Some(avatar.to_owned()),
None => read_user.avatar,
};
let password_encrypted = match &data.new_password { let password_encrypted = match &data.new_password {
Some(new_password) => { Some(new_password) => {
match &data.new_password_verify { match &data.new_password_verify {
@ -488,7 +495,7 @@ impl Perform for Oper<SaveUserSettings> {
name: read_user.name, name: read_user.name,
email, email,
matrix_user_id: data.matrix_user_id.to_owned(), matrix_user_id: data.matrix_user_id.to_owned(),
avatar: data.avatar.to_owned(), avatar,
password_encrypted, password_encrypted,
preferred_username: read_user.preferred_username, preferred_username: read_user.preferred_username,
updated: Some(naive_now()), updated: Some(naive_now()),
@ -527,7 +534,7 @@ impl Perform for Oper<SaveUserSettings> {
// Return the jwt // Return the jwt
Ok(LoginResponse { Ok(LoginResponse {
jwt: updated_user.jwt(), jwt: Claims::jwt(updated_user, Settings::get().hostname),
}) })
} }
} }
@ -678,7 +685,8 @@ impl Perform for Oper<AddAdmin> {
} }
let added = data.added; let added = data.added;
let add_admin = move |conn: &'_ _| User_::add_admin(conn, user_id, added); let added_user_id = data.user_id;
let add_admin = move |conn: &'_ _| User_::add_admin(conn, added_user_id, added);
if blocking(pool, add_admin).await?.is_err() { if blocking(pool, add_admin).await?.is_err() {
return Err(APIError::err("couldnt_update_user").into()); return Err(APIError::err("couldnt_update_user").into());
} }
@ -1149,7 +1157,7 @@ impl Perform for Oper<PasswordChange> {
// Return the jwt // Return the jwt
Ok(LoginResponse { Ok(LoginResponse {
jwt: updated_user.jwt(), jwt: Claims::jwt(updated_user, Settings::get().hostname),
}) })
} }
} }
@ -1207,7 +1215,12 @@ impl Perform for Oper<CreatePrivateMessage> {
let inserted_private_message_id = inserted_private_message.id; let inserted_private_message_id = inserted_private_message.id;
let updated_private_message = match blocking(pool, move |conn| { let updated_private_message = match blocking(pool, move |conn| {
PrivateMessage::update_ap_id(&conn, inserted_private_message_id) let apub_id = make_apub_endpoint(
EndpointType::PrivateMessage,
&inserted_private_message_id.to_string(),
)
.to_string();
PrivateMessage::update_ap_id(&conn, inserted_private_message_id, apub_id)
}) })
.await? .await?
{ {

View file

@ -1,12 +1,18 @@
use crate::{ use crate::{
apub::{extensions::signatures::sign, is_apub_id_valid, ActorType}, apub::{
db::{activity::insert_activity, community::Community, user::User_}, community::do_announce,
extensions::signatures::sign,
insert_activity,
is_apub_id_valid,
ActorType,
},
request::retry_custom, request::retry_custom,
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base}; use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base};
use actix_web::client::Client; use actix_web::client::Client;
use lemmy_db::{community::Community, user::User_};
use log::debug; use log::debug;
use serde::Serialize; use serde::Serialize;
use std::fmt::Debug; use std::fmt::Debug;
@ -43,7 +49,7 @@ where
// if this is a local community, we need to do an announce from the community instead // if this is a local community, we need to do an announce from the community instead
if community.local { if community.local {
Community::do_announce(activity, &community, creator, client, pool).await?; do_announce(activity, &community, creator, client, pool).await?;
} else { } else {
send_activity(client, &activity, creator, to).await?; send_activity(client, &activity, creator, to).await?;
} }

View file

@ -17,19 +17,9 @@ use crate::{
ToApub, ToApub,
}, },
blocking, blocking,
convert_datetime,
db::{
comment::{Comment, CommentForm},
community::Community,
post::Post,
user::User_,
Crud,
},
routes::DbPoolParam, routes::DbPoolParam,
scrape_text_for_mentions,
DbPool, DbPool,
LemmyError, LemmyError,
MentionData,
}; };
use activitystreams::{ use activitystreams::{
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update}, activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
@ -40,6 +30,14 @@ use activitystreams::{
use activitystreams_new::object::Tombstone; use activitystreams_new::object::Tombstone;
use actix_web::{body::Body, client::Client, web::Path, HttpResponse}; use actix_web::{body::Body, client::Client, web::Path, HttpResponse};
use itertools::Itertools; use itertools::Itertools;
use lemmy_db::{
comment::{Comment, CommentForm},
community::Community,
post::Post,
user::User_,
Crud,
};
use lemmy_utils::{convert_datetime, scrape_text_for_mentions, MentionData};
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
@ -123,7 +121,7 @@ impl FromApub for CommentForm {
/// Parse an ActivityPub note received from another instance into a Lemmy comment /// Parse an ActivityPub note received from another instance into a Lemmy comment
async fn from_apub( async fn from_apub(
note: &Note, note: &mut Note,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<CommentForm, LemmyError> { ) -> Result<CommentForm, LemmyError> {

View file

@ -4,44 +4,48 @@ use crate::{
create_apub_response, create_apub_response,
create_apub_tombstone_response, create_apub_tombstone_response,
create_tombstone, create_tombstone,
extensions::{group_extensions::GroupExtension, signatures::PublicKey}, extensions::group_extensions::GroupExtension,
fetcher::get_or_fetch_and_upsert_remote_user, fetcher::get_or_fetch_and_upsert_remote_user,
get_shared_inbox, get_shared_inbox,
insert_activity,
ActorType, ActorType,
FromApub, FromApub,
GroupExt, GroupExt,
ToApub, ToApub,
}, },
blocking, blocking,
convert_datetime,
db::{
activity::insert_activity,
community::{Community, CommunityForm},
community_view::{CommunityFollowerView, CommunityModeratorView},
user::User_,
},
naive_now,
routes::DbPoolParam, routes::DbPoolParam,
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use activitystreams::{ use activitystreams::{
activity::{Accept, Announce, Delete, Remove, Undo}, activity::{Accept, Announce, Delete, Remove, Undo},
actor::{kind::GroupType, properties::ApActorProperties, Group},
collection::UnorderedCollection,
context,
endpoint::EndpointProperties,
object::properties::ObjectProperties,
Activity, Activity,
Base, Base,
BaseBox, BaseBox,
}; };
use activitystreams_ext::Ext3; use activitystreams_ext::Ext2;
use activitystreams_new::{activity::Follow, object::Tombstone}; use activitystreams_new::{
activity::Follow,
actor::{kind::GroupType, ApActor, Endpoints, Group},
base::BaseExt,
collection::UnorderedCollection,
context,
object::Tombstone,
prelude::*,
primitives::{XsdAnyUri, XsdDateTime},
};
use actix_web::{body::Body, client::Client, web, HttpResponse}; use actix_web::{body::Body, client::Client, web, HttpResponse};
use itertools::Itertools; use itertools::Itertools;
use lemmy_db::{
community::{Community, CommunityForm},
community_view::{CommunityFollowerView, CommunityModeratorView},
naive_now,
user::User_,
};
use lemmy_utils::convert_datetime;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Debug; use std::{fmt::Debug, str::FromStr};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CommunityQuery { pub struct CommunityQuery {
@ -54,9 +58,6 @@ impl ToApub for Community {
// Turn a Lemmy Community into an ActivityPub group that can be sent out over the network. // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> { async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> {
let mut group = Group::default();
let oprops: &mut ObjectProperties = group.as_mut();
// The attributed to, is an ordered vector with the creator actor_ids first, // The attributed to, is an ordered vector with the creator actor_ids first,
// then the rest of the moderators // then the rest of the moderators
// TODO Technically the instance admins can mod the community, but lets // TODO Technically the instance admins can mod the community, but lets
@ -66,36 +67,36 @@ impl ToApub for Community {
CommunityModeratorView::for_community(&conn, id) CommunityModeratorView::for_community(&conn, id)
}) })
.await??; .await??;
let moderators = moderators.into_iter().map(|m| m.user_actor_id).collect(); let moderators: Vec<String> = moderators.into_iter().map(|m| m.user_actor_id).collect();
oprops let mut group = Group::new();
.set_context_xsd_any_uri(context())? group
.set_id(self.actor_id.to_owned())? .set_context(context())
.set_name_xsd_string(self.name.to_owned())? .set_id(XsdAnyUri::from_str(&self.actor_id)?)
.set_published(convert_datetime(self.published))? .set_name(self.name.to_owned())
.set_many_attributed_to_xsd_any_uris(moderators)?; .set_published(XsdDateTime::from(convert_datetime(self.published)))
.set_many_attributed_tos(moderators);
if let Some(u) = self.updated.to_owned() { if let Some(u) = self.updated.to_owned() {
oprops.set_updated(convert_datetime(u))?; group.set_updated(XsdDateTime::from(convert_datetime(u)));
} }
if let Some(d) = self.description.to_owned() { if let Some(d) = self.description.to_owned() {
// TODO: this should be html, also add source field with raw markdown // TODO: this should be html, also add source field with raw markdown
// -> same for post.content and others // -> same for post.content and others
oprops.set_content_xsd_string(d)?; group.set_content(d);
} }
let mut endpoint_props = EndpointProperties::default(); let mut ap_actor = ApActor::new(self.get_inbox_url().parse()?, group);
ap_actor
endpoint_props.set_shared_inbox(self.get_shared_inbox_url())?; .set_preferred_username(self.title.to_owned())
.set_outbox(self.get_outbox_url().parse()?)
let mut actor_props = ApActorProperties::default(); .set_followers(self.get_followers_url().parse()?)
.set_following(self.get_following_url().parse()?)
actor_props .set_liked(self.get_liked_url().parse()?)
.set_preferred_username(self.title.to_owned())? .set_endpoints(Endpoints {
.set_inbox(self.get_inbox_url())? shared_inbox: Some(self.get_shared_inbox_url().parse()?),
.set_outbox(self.get_outbox_url())? ..Default::default()
.set_endpoints(endpoint_props)? });
.set_followers(self.get_followers_url())?;
let nsfw = self.nsfw; let nsfw = self.nsfw;
let category_id = self.category_id; let category_id = self.category_id;
@ -104,10 +105,9 @@ impl ToApub for Community {
}) })
.await??; .await??;
Ok(Ext3::new( Ok(Ext2::new(
group, ap_actor,
group_extension, group_extension,
actor_props,
self.get_public_key_ext(), self.get_public_key_ext(),
)) ))
} }
@ -367,38 +367,52 @@ impl FromApub for CommunityForm {
type ApubType = GroupExt; type ApubType = GroupExt;
/// Parse an ActivityPub group received from another instance into a Lemmy community. /// Parse an ActivityPub group received from another instance into a Lemmy community.
async fn from_apub(group: &GroupExt, client: &Client, pool: &DbPool) -> Result<Self, LemmyError> { async fn from_apub(
let group_extensions: &GroupExtension = &group.ext_one; group: &mut GroupExt,
let oprops = &group.inner.object_props; client: &Client,
let aprops = &group.ext_two; pool: &DbPool,
let public_key: &PublicKey = &group.ext_three.public_key; ) -> Result<Self, LemmyError> {
// TODO: this is probably gonna cause problems cause fetcher:292 also calls take_attributed_to()
let mut creator_and_moderator_uris = oprops.get_many_attributed_to_xsd_any_uris().unwrap(); let creator_and_moderator_uris = group.clone().take_attributed_to().unwrap();
let creator_uri = creator_and_moderator_uris.next().unwrap(); let creator_uri = creator_and_moderator_uris
.as_many()
.unwrap()
.iter()
.next()
.unwrap()
.as_xsd_any_uri()
.unwrap();
let creator = get_or_fetch_and_upsert_remote_user(creator_uri.as_str(), client, pool).await?; let creator = get_or_fetch_and_upsert_remote_user(creator_uri.as_str(), client, pool).await?;
Ok(CommunityForm { Ok(CommunityForm {
name: oprops.get_name_xsd_string().unwrap().to_string(), name: group
title: aprops.get_preferred_username().unwrap().to_string(), .take_name()
.unwrap()
.as_single_xsd_string()
.unwrap()
.into(),
title: group.inner.take_preferred_username().unwrap(),
// TODO: should be parsed as html and tags like <script> removed (or use markdown source) // TODO: should be parsed as html and tags like <script> removed (or use markdown source)
// -> same for post.content etc // -> same for post.content etc
description: oprops.get_content_xsd_string().map(|s| s.to_string()), description: group
category_id: group_extensions.category.identifier.parse::<i32>()?, .take_content()
.map(|s| s.as_single_xsd_string().unwrap().into()),
category_id: group.ext_one.category.identifier.parse::<i32>()?,
creator_id: creator.id, creator_id: creator.id,
removed: None, removed: None,
published: oprops published: group
.get_published() .take_published()
.map(|u| u.as_ref().to_owned().naive_local()), .map(|u| u.as_ref().to_owned().naive_local()),
updated: oprops updated: group
.get_updated() .take_updated()
.map(|u| u.as_ref().to_owned().naive_local()), .map(|u| u.as_ref().to_owned().naive_local()),
deleted: None, deleted: None,
nsfw: group_extensions.sensitive, nsfw: group.ext_one.sensitive,
actor_id: oprops.get_id().unwrap().to_string(), actor_id: group.id().unwrap().to_string(),
local: false, local: false,
private_key: None, private_key: None,
public_key: Some(public_key.to_owned().public_key_pem), public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
last_refreshed_at: Some(naive_now()), last_refreshed_at: Some(naive_now()),
}) })
} }
@ -439,50 +453,46 @@ pub async fn get_apub_community_followers(
}) })
.await??; .await??;
let mut collection = UnorderedCollection::default(); let mut collection = UnorderedCollection::new(vec![]);
let oprops: &mut ObjectProperties = collection.as_mut();
oprops
.set_context_xsd_any_uri(context())?
.set_id(community.actor_id)?;
collection collection
.collection_props .set_context(context())
.set_total_items(community_followers.len() as u64)?; // TODO: this needs its own ID
.set_id(community.actor_id.parse()?)
.set_total_items(community_followers.len() as u64);
Ok(create_apub_response(&collection)) Ok(create_apub_response(&collection))
} }
impl Community { pub async fn do_announce<A>(
pub async fn do_announce<A>( activity: A,
activity: A, community: &Community,
community: &Community, sender: &dyn ActorType,
sender: &dyn ActorType, client: &Client,
client: &Client, pool: &DbPool,
pool: &DbPool, ) -> Result<HttpResponse, LemmyError>
) -> Result<HttpResponse, LemmyError> where
where A: Activity + Base + Serialize + Debug,
A: Activity + Base + Serialize + Debug, {
{ let mut announce = Announce::default();
let mut announce = Announce::default(); populate_object_props(
populate_object_props( &mut announce.object_props,
&mut announce.object_props, vec![community.get_followers_url()],
vec![community.get_followers_url()], &format!("{}/announce/{}", community.actor_id, uuid::Uuid::new_v4()),
&format!("{}/announce/{}", community.actor_id, uuid::Uuid::new_v4()), )?;
)?; announce
announce .announce_props
.announce_props .set_actor_xsd_any_uri(community.actor_id.to_owned())?
.set_actor_xsd_any_uri(community.actor_id.to_owned())? .set_object_base_box(BaseBox::from_concrete(activity)?)?;
.set_object_base_box(BaseBox::from_concrete(activity)?)?;
insert_activity(community.creator_id, announce.clone(), true, pool).await?; insert_activity(community.creator_id, announce.clone(), true, pool).await?;
// dont send to the instance where the activity originally came from, because that would result // dont send to the instance where the activity originally came from, because that would result
// in a database error (same data inserted twice) // in a database error (same data inserted twice)
let mut to = community.get_follower_inboxes(pool).await?; let mut to = community.get_follower_inboxes(pool).await?;
// this seems to be the "easiest" stable alternative for remove_item() // this seems to be the "easiest" stable alternative for remove_item()
to.retain(|x| *x != sender.get_shared_inbox_url()); to.retain(|x| *x != sender.get_shared_inbox_url());
send_activity(client, &announce, community, to).await?; send_activity(client, &announce, community, to).await?;
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
}
} }

View file

@ -2,21 +2,21 @@ use crate::{
apub::{ apub::{
extensions::signatures::verify, extensions::signatures::verify,
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}, fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
insert_activity,
ActorType, ActorType,
}, },
blocking, blocking,
db::{
activity::insert_activity,
community::{Community, CommunityFollower, CommunityFollowerForm},
user::User_,
Followable,
},
routes::{ChatServerParam, DbPoolParam}, routes::{ChatServerParam, DbPoolParam},
LemmyError, LemmyError,
}; };
use activitystreams::activity::Undo; use activitystreams::activity::Undo;
use activitystreams_new::activity::Follow; use activitystreams_new::activity::Follow;
use actix_web::{client::Client, web, HttpRequest, HttpResponse}; use actix_web::{client::Client, web, HttpRequest, HttpResponse};
use lemmy_db::{
community::{Community, CommunityFollower, CommunityFollowerForm},
user::User_,
Followable,
};
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
use std::fmt::Debug; use std::fmt::Debug;

View file

@ -1,9 +1,7 @@
use crate::{ use crate::LemmyError;
db::{category::Category, Crud},
LemmyError,
};
use activitystreams::{ext::Extension, Actor}; use activitystreams::{ext::Extension, Actor};
use diesel::PgConnection; use diesel::PgConnection;
use lemmy_db::{category::Category, Crud};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]

View file

@ -9,7 +9,6 @@ use log::debug;
use openssl::{ use openssl::{
hash::MessageDigest, hash::MessageDigest,
pkey::PKey, pkey::PKey,
rsa::Rsa,
sign::{Signer, Verifier}, sign::{Signer, Verifier},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -19,23 +18,6 @@ lazy_static! {
static ref HTTP_SIG_CONFIG: Config = Config::new(); static ref HTTP_SIG_CONFIG: Config = Config::new();
} }
pub struct Keypair {
pub private_key: String,
pub public_key: String,
}
/// Generate the asymmetric keypair for ActivityPub HTTP signatures.
pub fn generate_actor_keypair() -> Result<Keypair, LemmyError> {
let rsa = Rsa::generate(2048)?;
let pkey = PKey::from_rsa(rsa)?;
let public_key = pkey.public_key_to_pem()?;
let private_key = pkey.private_key_to_pem_pkcs8()?;
Ok(Keypair {
private_key: String::from_utf8(private_key)?,
public_key: String::from_utf8(public_key)?,
})
}
/// Signs request headers with the given keypair. /// Signs request headers with the given keypair.
pub async fn sign( pub async fn sign(
request: ClientRequest, request: ClientRequest,

View file

@ -1,46 +1,36 @@
use activitystreams::object::Note;
use actix_web::client::Client;
use diesel::{result::Error::NotFound, PgConnection};
use log::debug;
use serde::Deserialize;
use std::{fmt::Debug, time::Duration};
use url::Url;
use crate::{ use crate::{
api::site::SearchResponse, api::site::SearchResponse,
apub::{is_apub_id_valid, FromApub, GroupExt, PageExt, PersonExt, APUB_JSON_CONTENT_TYPE},
blocking, blocking,
db::{
comment::{Comment, CommentForm},
comment_view::CommentView,
community::{Community, CommunityForm, CommunityModerator, CommunityModeratorForm},
community_view::CommunityView,
post::{Post, PostForm},
post_view::PostView,
user::{UserForm, User_},
Crud,
Joinable,
SearchType,
},
naive_now,
request::{retry, RecvError}, request::{retry, RecvError},
routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}, routes::nodeinfo::{NodeInfo, NodeInfoWellKnown},
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use activitystreams::object::Note;
use crate::{ use activitystreams_new::{base::BaseExt, prelude::*, primitives::XsdAnyUri};
apub::{ use actix_web::client::Client;
get_apub_protocol_string,
is_apub_id_valid,
FromApub,
GroupExt,
PageExt,
PersonExt,
APUB_JSON_CONTENT_TYPE,
},
db::user_view::UserView,
};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{result::Error::NotFound, PgConnection};
use lemmy_db::{
comment::{Comment, CommentForm},
comment_view::CommentView,
community::{Community, CommunityForm, CommunityModerator, CommunityModeratorForm},
community_view::CommunityView,
naive_now,
post::{Post, PostForm},
post_view::PostView,
user::{UserForm, User_},
user_view::UserView,
Crud,
Joinable,
SearchType,
};
use lemmy_utils::get_apub_protocol_string;
use log::debug;
use serde::Deserialize;
use std::{fmt::Debug, time::Duration};
use url::Url;
static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60; static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60;
@ -149,7 +139,7 @@ pub async fn search_by_apub_id(
let response = match fetch_remote_object::<SearchAcceptedObjects>(client, &query_url).await? { let response = match fetch_remote_object::<SearchAcceptedObjects>(client, &query_url).await? {
SearchAcceptedObjects::Person(p) => { SearchAcceptedObjects::Person(p) => {
let user_uri = p.inner.object_props.get_id().unwrap().to_string(); let user_uri = p.inner.id().unwrap().to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
@ -158,7 +148,7 @@ pub async fn search_by_apub_id(
response response
} }
SearchAcceptedObjects::Group(g) => { SearchAcceptedObjects::Group(g) => {
let community_uri = g.inner.object_props.get_id().unwrap().to_string(); let community_uri = g.inner.id().unwrap().to_string();
let community = let community =
get_or_fetch_and_upsert_remote_community(&community_uri, client, pool).await?; get_or_fetch_and_upsert_remote_community(&community_uri, client, pool).await?;
@ -174,15 +164,15 @@ pub async fn search_by_apub_id(
response response
} }
SearchAcceptedObjects::Page(p) => { SearchAcceptedObjects::Page(mut p) => {
let post_form = PostForm::from_apub(&p, client, pool).await?; let post_form = PostForm::from_apub(&mut p, client, pool).await?;
let p = blocking(pool, move |conn| upsert_post(&post_form, conn)).await??; let p = blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
response.posts = vec![blocking(pool, move |conn| PostView::read(conn, p.id, None)).await??]; response.posts = vec![blocking(pool, move |conn| PostView::read(conn, p.id, None)).await??];
response response
} }
SearchAcceptedObjects::Comment(c) => { SearchAcceptedObjects::Comment(mut c) => {
let post_url = c let post_url = c
.object_props .object_props
.get_many_in_reply_to_xsd_any_uris() .get_many_in_reply_to_xsd_any_uris()
@ -192,9 +182,9 @@ pub async fn search_by_apub_id(
.to_string(); .to_string();
// TODO: also fetch parent comments if any // TODO: also fetch parent comments if any
let post = fetch_remote_object(client, &Url::parse(&post_url)?).await?; let mut post = fetch_remote_object(client, &Url::parse(&post_url)?).await?;
let post_form = PostForm::from_apub(&post, client, pool).await?; let post_form = PostForm::from_apub(&mut post, client, pool).await?;
let comment_form = CommentForm::from_apub(&c, client, pool).await?; let comment_form = CommentForm::from_apub(&mut c, client, pool).await?;
blocking(pool, move |conn| upsert_post(&post_form, conn)).await??; blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
let c = blocking(pool, move |conn| upsert_comment(&comment_form, conn)).await??; let c = blocking(pool, move |conn| upsert_comment(&comment_form, conn)).await??;
@ -224,9 +214,9 @@ pub async fn get_or_fetch_and_upsert_remote_user(
// If its older than a day, re-fetch it // If its older than a day, re-fetch it
Ok(u) if !u.local && should_refetch_actor(u.last_refreshed_at) => { Ok(u) if !u.local && should_refetch_actor(u.last_refreshed_at) => {
debug!("Fetching and updating from remote user: {}", apub_id); debug!("Fetching and updating from remote user: {}", apub_id);
let person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?; let mut person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?;
let mut uf = UserForm::from_apub(&person, client, pool).await?; let mut uf = UserForm::from_apub(&mut person, client, pool).await?;
uf.last_refreshed_at = Some(naive_now()); uf.last_refreshed_at = Some(naive_now());
let user = blocking(pool, move |conn| User_::update(conn, u.id, &uf)).await??; let user = blocking(pool, move |conn| User_::update(conn, u.id, &uf)).await??;
@ -235,9 +225,9 @@ pub async fn get_or_fetch_and_upsert_remote_user(
Ok(u) => Ok(u), Ok(u) => Ok(u),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!("Fetching and creating remote user: {}", apub_id); debug!("Fetching and creating remote user: {}", apub_id);
let person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?; let mut person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?;
let uf = UserForm::from_apub(&person, client, pool).await?; let uf = UserForm::from_apub(&mut person, client, pool).await?;
let user = blocking(pool, move |conn| User_::create(conn, &uf)).await??; let user = blocking(pool, move |conn| User_::create(conn, &uf)).await??;
Ok(user) Ok(user)
@ -275,9 +265,9 @@ pub async fn get_or_fetch_and_upsert_remote_community(
match community { match community {
Ok(c) if !c.local && should_refetch_actor(c.last_refreshed_at) => { Ok(c) if !c.local && should_refetch_actor(c.last_refreshed_at) => {
debug!("Fetching and updating from remote community: {}", apub_id); debug!("Fetching and updating from remote community: {}", apub_id);
let group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?; let mut group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?;
let mut cf = CommunityForm::from_apub(&group, client, pool).await?; let mut cf = CommunityForm::from_apub(&mut group, client, pool).await?;
cf.last_refreshed_at = Some(naive_now()); cf.last_refreshed_at = Some(naive_now());
let community = blocking(pool, move |conn| Community::update(conn, c.id, &cf)).await??; let community = blocking(pool, move |conn| Community::update(conn, c.id, &cf)).await??;
@ -286,17 +276,19 @@ pub async fn get_or_fetch_and_upsert_remote_community(
Ok(c) => Ok(c), Ok(c) => Ok(c),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!("Fetching and creating remote community: {}", apub_id); debug!("Fetching and creating remote community: {}", apub_id);
let group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?; let mut group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?;
let cf = CommunityForm::from_apub(&group, client, pool).await?; let cf = CommunityForm::from_apub(&mut group, client, pool).await?;
let community = blocking(pool, move |conn| Community::create(conn, &cf)).await??; let community = blocking(pool, move |conn| Community::create(conn, &cf)).await??;
// Also add the community moderators too // Also add the community moderators too
let creator_and_moderator_uris = group let attributed_to = group.inner.take_attributed_to().unwrap();
.inner let creator_and_moderator_uris: Vec<&XsdAnyUri> = attributed_to
.object_props .as_many()
.get_many_attributed_to_xsd_any_uris() .unwrap()
.unwrap(); .iter()
.map(|a| a.as_xsd_any_uri().unwrap())
.collect();
let mut creator_and_moderators = Vec::new(); let mut creator_and_moderators = Vec::new();
@ -350,8 +342,8 @@ pub async fn get_or_fetch_and_insert_remote_post(
Ok(p) => Ok(p), Ok(p) => Ok(p),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!("Fetching and creating remote post: {}", post_ap_id); debug!("Fetching and creating remote post: {}", post_ap_id);
let post = fetch_remote_object::<PageExt>(client, &Url::parse(post_ap_id)?).await?; let mut post = fetch_remote_object::<PageExt>(client, &Url::parse(post_ap_id)?).await?;
let post_form = PostForm::from_apub(&post, client, pool).await?; let post_form = PostForm::from_apub(&mut post, client, pool).await?;
let post = blocking(pool, move |conn| Post::create(conn, &post_form)).await??; let post = blocking(pool, move |conn| Post::create(conn, &post_form)).await??;
@ -388,8 +380,8 @@ pub async fn get_or_fetch_and_insert_remote_comment(
"Fetching and creating remote comment and its parents: {}", "Fetching and creating remote comment and its parents: {}",
comment_ap_id comment_ap_id
); );
let comment = fetch_remote_object::<Note>(client, &Url::parse(comment_ap_id)?).await?; let mut comment = fetch_remote_object::<Note>(client, &Url::parse(comment_ap_id)?).await?;
let comment_form = CommentForm::from_apub(&comment, client, pool).await?; let comment_form = CommentForm::from_apub(&mut comment, client, pool).await?;
let comment = blocking(pool, move |conn| Comment::create(conn, &comment_form)).await??; let comment = blocking(pool, move |conn| Comment::create(conn, &comment_form)).await??;

View file

@ -16,41 +16,35 @@ use crate::{
page_extension::PageExtension, page_extension::PageExtension,
signatures::{PublicKey, PublicKeyExtension}, signatures::{PublicKey, PublicKeyExtension},
}, },
convert_datetime, blocking,
db::user::User_,
request::{retry, RecvError}, request::{retry, RecvError},
routes::webfinger::WebFingerResponse, routes::webfinger::WebFingerResponse,
DbPool, DbPool,
LemmyError, LemmyError,
MentionData,
Settings,
}; };
use activitystreams::{ use activitystreams::object::Page;
actor::{properties::ApActorProperties, Group, Person}, use activitystreams_ext::{Ext1, Ext2};
object::Page, use activitystreams_new::{
activity::Follow,
actor::{ApActor, Group, Person},
object::Tombstone,
prelude::*,
}; };
use activitystreams_ext::{Ext1, Ext2, Ext3};
use activitystreams_new::{activity::Follow, object::Tombstone, prelude::*};
use actix_web::{body::Body, client::Client, HttpResponse}; use actix_web::{body::Body, client::Client, HttpResponse};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use failure::_core::fmt::Debug;
use lemmy_db::{activity::do_insert_activity, user::User_};
use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings, MentionData};
use log::debug; use log::debug;
use serde::Serialize; use serde::Serialize;
use url::Url; use url::Url;
type GroupExt = Ext3<Group, GroupExtension, ApActorProperties, PublicKeyExtension>; type GroupExt = Ext2<ApActor<Group>, GroupExtension, PublicKeyExtension>;
type PersonExt = Ext2<Person, ApActorProperties, PublicKeyExtension>; type PersonExt = Ext1<ApActor<Person>, PublicKeyExtension>;
type PageExt = Ext1<Page, PageExtension>; type PageExt = Ext1<Page, PageExtension>;
pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json"; pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
pub enum EndpointType {
Community,
User,
Post,
Comment,
PrivateMessage,
}
/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub /// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
/// headers. /// headers.
fn create_apub_response<T>(data: &T) -> HttpResponse<Body> fn create_apub_response<T>(data: &T) -> HttpResponse<Body>
@ -71,34 +65,6 @@ where
.json(data) .json(data)
} }
/// Generates the ActivityPub ID for a given object type and ID.
pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
let point = match endpoint_type {
EndpointType::Community => "c",
EndpointType::User => "u",
EndpointType::Post => "post",
EndpointType::Comment => "comment",
EndpointType::PrivateMessage => "private_message",
};
Url::parse(&format!(
"{}://{}/{}/{}",
get_apub_protocol_string(),
Settings::get().hostname,
point,
name
))
.unwrap()
}
pub fn get_apub_protocol_string() -> &'static str {
if Settings::get().federation.tls_enabled {
"https"
} else {
"http"
}
}
// Checks if the ID has a valid format, correct scheme, and is in the allowed instance list. // Checks if the ID has a valid format, correct scheme, and is in the allowed instance list.
fn is_apub_id_valid(apub_id: &Url) -> bool { fn is_apub_id_valid(apub_id: &Url) -> bool {
debug!("Checking {}", apub_id); debug!("Checking {}", apub_id);
@ -163,7 +129,7 @@ fn create_tombstone(
pub trait FromApub { pub trait FromApub {
type ApubType; type ApubType;
async fn from_apub( async fn from_apub(
apub: &Self::ApubType, apub: &mut Self::ApubType,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<Self, LemmyError> ) -> Result<Self, LemmyError>
@ -372,3 +338,19 @@ pub async fn fetch_webfinger_url(
.to_owned() .to_owned()
.ok_or_else(|| format_err!("No href found.").into()) .ok_or_else(|| format_err!("No href found.").into())
} }
pub async fn insert_activity<T>(
user_id: i32,
data: T,
local: bool,
pool: &DbPool,
) -> Result<(), LemmyError>
where
T: Serialize + Debug + Send + 'static,
{
blocking(pool, move |conn| {
do_insert_activity(conn, user_id, &data, local)
})
.await??;
Ok(())
}

View file

@ -6,7 +6,6 @@ use crate::{
create_tombstone, create_tombstone,
extensions::page_extension::PageExtension, extensions::page_extension::PageExtension,
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}, fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
get_apub_protocol_string,
ActorType, ActorType,
ApubLikeableType, ApubLikeableType,
ApubObjectType, ApubObjectType,
@ -15,17 +14,9 @@ use crate::{
ToApub, ToApub,
}, },
blocking, blocking,
convert_datetime,
db::{
community::Community,
post::{Post, PostForm},
user::User_,
Crud,
},
routes::DbPoolParam, routes::DbPoolParam,
DbPool, DbPool,
LemmyError, LemmyError,
Settings,
}; };
use activitystreams::{ use activitystreams::{
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update}, activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
@ -36,6 +27,13 @@ use activitystreams::{
use activitystreams_ext::Ext1; use activitystreams_ext::Ext1;
use activitystreams_new::object::Tombstone; use activitystreams_new::object::Tombstone;
use actix_web::{body::Body, client::Client, web, HttpResponse}; use actix_web::{body::Body, client::Client, web, HttpResponse};
use lemmy_db::{
community::Community,
post::{Post, PostForm},
user::User_,
Crud,
};
use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings};
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -164,7 +162,7 @@ impl FromApub for PostForm {
/// Parse an ActivityPub page received from another instance into a Lemmy post. /// Parse an ActivityPub page received from another instance into a Lemmy post.
async fn from_apub( async fn from_apub(
page: &PageExt, page: &mut PageExt,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<PostForm, LemmyError> { ) -> Result<PostForm, LemmyError> {

View file

@ -3,18 +3,12 @@ use crate::{
activities::send_activity, activities::send_activity,
create_tombstone, create_tombstone,
fetcher::get_or_fetch_and_upsert_remote_user, fetcher::get_or_fetch_and_upsert_remote_user,
insert_activity,
ApubObjectType, ApubObjectType,
FromApub, FromApub,
ToApub, ToApub,
}, },
blocking, blocking,
convert_datetime,
db::{
activity::insert_activity,
private_message::{PrivateMessage, PrivateMessageForm},
user::User_,
Crud,
},
DbPool, DbPool,
LemmyError, LemmyError,
}; };
@ -25,6 +19,12 @@ use activitystreams::{
}; };
use activitystreams_new::object::Tombstone; use activitystreams_new::object::Tombstone;
use actix_web::client::Client; use actix_web::client::Client;
use lemmy_db::{
private_message::{PrivateMessage, PrivateMessageForm},
user::User_,
Crud,
};
use lemmy_utils::convert_datetime;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ToApub for PrivateMessage { impl ToApub for PrivateMessage {
@ -71,7 +71,7 @@ impl FromApub for PrivateMessageForm {
/// Parse an ActivityPub note received from another instance into a Lemmy Private message /// Parse an ActivityPub note received from another instance into a Lemmy Private message
async fn from_apub( async fn from_apub(
note: &Note, note: &mut Note,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<PrivateMessageForm, LemmyError> { ) -> Result<PrivateMessageForm, LemmyError> {

View file

@ -5,6 +5,7 @@ use crate::{
post::PostResponse, post::PostResponse,
}, },
apub::{ apub::{
community::do_announce,
extensions::signatures::verify, extensions::signatures::verify,
fetcher::{ fetcher::{
get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_comment,
@ -12,25 +13,13 @@ use crate::{
get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_community,
get_or_fetch_and_upsert_remote_user, get_or_fetch_and_upsert_remote_user,
}, },
insert_activity,
FromApub, FromApub,
GroupExt, GroupExt,
PageExt, PageExt,
}, },
blocking, blocking,
db::{
activity::insert_activity,
comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
comment_view::CommentView,
community::{Community, CommunityForm},
community_view::CommunityView,
post::{Post, PostForm, PostLike, PostLikeForm},
post_view::PostView,
Crud,
Likeable,
},
naive_now,
routes::{ChatServerParam, DbPoolParam}, routes::{ChatServerParam, DbPoolParam},
scrape_text_for_mentions,
websocket::{ websocket::{
server::{SendComment, SendCommunityRoomMessage, SendPost}, server::{SendComment, SendCommunityRoomMessage, SendPost},
UserOperation, UserOperation,
@ -46,6 +35,18 @@ use activitystreams::{
BaseBox, BaseBox,
}; };
use actix_web::{client::Client, web, HttpRequest, HttpResponse}; use actix_web::{client::Client, web, HttpRequest, HttpResponse};
use lemmy_db::{
comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
comment_view::CommentView,
community::{Community, CommunityForm},
community_view::CommunityView,
naive_now,
post::{Post, PostForm, PostLike, PostLikeForm},
post_view::PostView,
Crud,
Likeable,
};
use lemmy_utils::scrape_text_for_mentions;
use log::debug; use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Debug; use std::fmt::Debug;
@ -234,7 +235,7 @@ where
if community.local { if community.local {
let sending_user = get_or_fetch_and_upsert_remote_user(sender, client, pool).await?; let sending_user = get_or_fetch_and_upsert_remote_user(sender, client, pool).await?;
Community::do_announce(activity, &community, &sending_user, client, pool).await do_announce(activity, &community, &sending_user, client, pool).await
} else { } else {
Ok(HttpResponse::NotFound().finish()) Ok(HttpResponse::NotFound().finish())
} }
@ -335,7 +336,7 @@ async fn receive_create_post(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let page = create let mut page = create
.create_props .create_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -353,7 +354,7 @@ async fn receive_create_post(
insert_activity(user.id, create, false, pool).await?; insert_activity(user.id, create, false, pool).await?;
let post = PostForm::from_apub(&page, client, pool).await?; let post = PostForm::from_apub(&mut page, client, pool).await?;
let inserted_post = blocking(pool, move |conn| Post::create(conn, &post)).await??; let inserted_post = blocking(pool, move |conn| Post::create(conn, &post)).await??;
@ -381,7 +382,7 @@ async fn receive_create_comment(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let note = create let mut note = create
.create_props .create_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -399,7 +400,7 @@ async fn receive_create_comment(
insert_activity(user.id, create, false, pool).await?; insert_activity(user.id, create, false, pool).await?;
let comment = CommentForm::from_apub(&note, client, pool).await?; let comment = CommentForm::from_apub(&mut note, client, pool).await?;
let inserted_comment = blocking(pool, move |conn| Comment::create(conn, &comment)).await??; let inserted_comment = blocking(pool, move |conn| Comment::create(conn, &comment)).await??;
@ -440,7 +441,7 @@ async fn receive_update_post(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let page = update let mut page = update
.update_props .update_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -458,7 +459,7 @@ async fn receive_update_post(
insert_activity(user.id, update, false, pool).await?; insert_activity(user.id, update, false, pool).await?;
let post = PostForm::from_apub(&page, client, pool).await?; let post = PostForm::from_apub(&mut page, client, pool).await?;
let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool) let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool)
.await? .await?
@ -486,7 +487,7 @@ async fn receive_like_post(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let page = like let mut page = like
.like_props .like_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -500,7 +501,7 @@ async fn receive_like_post(
insert_activity(user.id, like, false, pool).await?; insert_activity(user.id, like, false, pool).await?;
let post = PostForm::from_apub(&page, client, pool).await?; let post = PostForm::from_apub(&mut page, client, pool).await?;
let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool) let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool)
.await? .await?
@ -537,7 +538,7 @@ async fn receive_dislike_post(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let page = dislike let mut page = dislike
.dislike_props .dislike_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -555,7 +556,7 @@ async fn receive_dislike_post(
insert_activity(user.id, dislike, false, pool).await?; insert_activity(user.id, dislike, false, pool).await?;
let post = PostForm::from_apub(&page, client, pool).await?; let post = PostForm::from_apub(&mut page, client, pool).await?;
let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool) let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool)
.await? .await?
@ -592,7 +593,7 @@ async fn receive_update_comment(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let note = update let mut note = update
.update_props .update_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -610,7 +611,7 @@ async fn receive_update_comment(
insert_activity(user.id, update, false, pool).await?; insert_activity(user.id, update, false, pool).await?;
let comment = CommentForm::from_apub(&note, client, pool).await?; let comment = CommentForm::from_apub(&mut note, client, pool).await?;
let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool) let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
.await? .await?
@ -651,7 +652,7 @@ async fn receive_like_comment(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let note = like let mut note = like
.like_props .like_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -665,7 +666,7 @@ async fn receive_like_comment(
insert_activity(user.id, like, false, pool).await?; insert_activity(user.id, like, false, pool).await?;
let comment = CommentForm::from_apub(&note, client, pool).await?; let comment = CommentForm::from_apub(&mut note, client, pool).await?;
let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool) let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
.await? .await?
@ -709,7 +710,7 @@ async fn receive_dislike_comment(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let note = dislike let mut note = dislike
.dislike_props .dislike_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -727,7 +728,7 @@ async fn receive_dislike_comment(
insert_activity(user.id, dislike, false, pool).await?; insert_activity(user.id, dislike, false, pool).await?;
let comment = CommentForm::from_apub(&note, client, pool).await?; let comment = CommentForm::from_apub(&mut note, client, pool).await?;
let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool) let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
.await? .await?
@ -777,7 +778,7 @@ async fn receive_delete_community(
.unwrap() .unwrap()
.to_string(); .to_string();
let group = delete let mut group = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -789,7 +790,7 @@ async fn receive_delete_community(
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let community_actor_id = CommunityForm::from_apub(&group, client, pool) let community_actor_id = CommunityForm::from_apub(&mut group, client, pool)
.await? .await?
.actor_id; .actor_id;
@ -854,7 +855,7 @@ async fn receive_remove_community(
.unwrap() .unwrap()
.to_string(); .to_string();
let group = remove let mut group = remove
.remove_props .remove_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -866,7 +867,7 @@ async fn receive_remove_community(
insert_activity(mod_.id, remove, false, pool).await?; insert_activity(mod_.id, remove, false, pool).await?;
let community_actor_id = CommunityForm::from_apub(&group, client, pool) let community_actor_id = CommunityForm::from_apub(&mut group, client, pool)
.await? .await?
.actor_id; .actor_id;
@ -931,7 +932,7 @@ async fn receive_delete_post(
.unwrap() .unwrap()
.to_string(); .to_string();
let page = delete let mut page = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -943,7 +944,7 @@ async fn receive_delete_post(
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id; let post_ap_id = PostForm::from_apub(&mut page, client, pool).await?.ap_id;
let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?; let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
@ -997,7 +998,7 @@ async fn receive_remove_post(
.unwrap() .unwrap()
.to_string(); .to_string();
let page = remove let mut page = remove
.remove_props .remove_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1009,7 +1010,7 @@ async fn receive_remove_post(
insert_activity(mod_.id, remove, false, pool).await?; insert_activity(mod_.id, remove, false, pool).await?;
let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id; let post_ap_id = PostForm::from_apub(&mut page, client, pool).await?.ap_id;
let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?; let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
@ -1063,7 +1064,7 @@ async fn receive_delete_comment(
.unwrap() .unwrap()
.to_string(); .to_string();
let note = delete let mut note = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1075,7 +1076,7 @@ async fn receive_delete_comment(
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let comment_ap_id = CommentForm::from_apub(&note, client, pool).await?.ap_id; let comment_ap_id = CommentForm::from_apub(&mut note, client, pool).await?.ap_id;
let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?; let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?;
@ -1131,7 +1132,7 @@ async fn receive_remove_comment(
.unwrap() .unwrap()
.to_string(); .to_string();
let note = remove let mut note = remove
.remove_props .remove_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1143,7 +1144,7 @@ async fn receive_remove_comment(
insert_activity(mod_.id, remove, false, pool).await?; insert_activity(mod_.id, remove, false, pool).await?;
let comment_ap_id = CommentForm::from_apub(&note, client, pool).await?.ap_id; let comment_ap_id = CommentForm::from_apub(&mut note, client, pool).await?.ap_id;
let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?; let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?;
@ -1259,7 +1260,7 @@ async fn receive_undo_delete_comment(
.unwrap() .unwrap()
.to_string(); .to_string();
let note = delete let mut note = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1271,7 +1272,7 @@ async fn receive_undo_delete_comment(
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let comment_ap_id = CommentForm::from_apub(&note, client, pool).await?.ap_id; let comment_ap_id = CommentForm::from_apub(&mut note, client, pool).await?.ap_id;
let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?; let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?;
@ -1327,7 +1328,7 @@ async fn receive_undo_remove_comment(
.unwrap() .unwrap()
.to_string(); .to_string();
let note = remove let mut note = remove
.remove_props .remove_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1339,7 +1340,7 @@ async fn receive_undo_remove_comment(
insert_activity(mod_.id, remove, false, pool).await?; insert_activity(mod_.id, remove, false, pool).await?;
let comment_ap_id = CommentForm::from_apub(&note, client, pool).await?.ap_id; let comment_ap_id = CommentForm::from_apub(&mut note, client, pool).await?.ap_id;
let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?; let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?;
@ -1395,7 +1396,7 @@ async fn receive_undo_delete_post(
.unwrap() .unwrap()
.to_string(); .to_string();
let page = delete let mut page = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1407,7 +1408,7 @@ async fn receive_undo_delete_post(
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id; let post_ap_id = PostForm::from_apub(&mut page, client, pool).await?.ap_id;
let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?; let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
@ -1461,7 +1462,7 @@ async fn receive_undo_remove_post(
.unwrap() .unwrap()
.to_string(); .to_string();
let page = remove let mut page = remove
.remove_props .remove_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1473,7 +1474,7 @@ async fn receive_undo_remove_post(
insert_activity(mod_.id, remove, false, pool).await?; insert_activity(mod_.id, remove, false, pool).await?;
let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id; let post_ap_id = PostForm::from_apub(&mut page, client, pool).await?.ap_id;
let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?; let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
@ -1527,7 +1528,7 @@ async fn receive_undo_delete_community(
.unwrap() .unwrap()
.to_string(); .to_string();
let group = delete let mut group = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1539,7 +1540,7 @@ async fn receive_undo_delete_community(
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let community_actor_id = CommunityForm::from_apub(&group, client, pool) let community_actor_id = CommunityForm::from_apub(&mut group, client, pool)
.await? .await?
.actor_id; .actor_id;
@ -1604,7 +1605,7 @@ async fn receive_undo_remove_community(
.unwrap() .unwrap()
.to_string(); .to_string();
let group = remove let mut group = remove
.remove_props .remove_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1616,7 +1617,7 @@ async fn receive_undo_remove_community(
insert_activity(mod_.id, remove, false, pool).await?; insert_activity(mod_.id, remove, false, pool).await?;
let community_actor_id = CommunityForm::from_apub(&group, client, pool) let community_actor_id = CommunityForm::from_apub(&mut group, client, pool)
.await? .await?
.actor_id; .actor_id;
@ -1704,7 +1705,7 @@ async fn receive_undo_like_comment(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let note = like let mut note = like
.like_props .like_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1718,7 +1719,7 @@ async fn receive_undo_like_comment(
insert_activity(user.id, like, false, pool).await?; insert_activity(user.id, like, false, pool).await?;
let comment = CommentForm::from_apub(&note, client, pool).await?; let comment = CommentForm::from_apub(&mut note, client, pool).await?;
let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool) let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
.await? .await?
@ -1758,7 +1759,7 @@ async fn receive_undo_like_post(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let page = like let mut page = like
.like_props .like_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1772,7 +1773,7 @@ async fn receive_undo_like_post(
insert_activity(user.id, like, false, pool).await?; insert_activity(user.id, like, false, pool).await?;
let post = PostForm::from_apub(&page, client, pool).await?; let post = PostForm::from_apub(&mut page, client, pool).await?;
let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool) let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool)
.await? .await?

View file

@ -1,38 +1,35 @@
use crate::{ use crate::{
api::claims::Claims,
apub::{ apub::{
activities::send_activity, activities::send_activity,
create_apub_response, create_apub_response,
extensions::signatures::PublicKey, insert_activity,
ActorType, ActorType,
FromApub, FromApub,
PersonExt, PersonExt,
ToApub, ToApub,
}, },
blocking, blocking,
convert_datetime,
db::{
activity::insert_activity,
user::{UserForm, User_},
},
naive_now,
routes::DbPoolParam, routes::DbPoolParam,
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use activitystreams::{ use activitystreams_ext::Ext1;
actor::{properties::ApActorProperties, Person},
context,
endpoint::EndpointProperties,
object::{properties::ObjectProperties, AnyImage, Image},
primitives::XsdAnyUri,
};
use activitystreams_ext::Ext2;
use activitystreams_new::{ use activitystreams_new::{
activity::{Follow, Undo}, activity::{Follow, Undo},
object::Tombstone, actor::{ApActor, Endpoints, Person},
context,
object::{Image, Tombstone},
prelude::*, prelude::*,
primitives::{XsdAnyUri, XsdDateTime},
}; };
use actix_web::{body::Body, client::Client, web, HttpResponse}; use actix_web::{body::Body, client::Client, web, HttpResponse};
use failure::_core::str::FromStr;
use lemmy_db::{
naive_now,
user::{UserForm, User_},
};
use lemmy_utils::convert_datetime;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -47,46 +44,39 @@ impl ToApub for User_ {
// Turn a Lemmy Community into an ActivityPub group that can be sent out over the network. // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
async fn to_apub(&self, _pool: &DbPool) -> Result<PersonExt, LemmyError> { async fn to_apub(&self, _pool: &DbPool) -> Result<PersonExt, LemmyError> {
// TODO go through all these to_string and to_owned() // TODO go through all these to_string and to_owned()
let mut person = Person::default(); let mut person = Person::new();
let oprops: &mut ObjectProperties = person.as_mut(); person
oprops .set_context(context())
.set_context_xsd_any_uri(context())? .set_id(XsdAnyUri::from_str(&self.actor_id)?)
.set_id(self.actor_id.to_string())? .set_name(self.name.to_owned())
.set_name_xsd_string(self.name.to_owned())? .set_published(XsdDateTime::from(convert_datetime(self.published)));
.set_published(convert_datetime(self.published))?;
if let Some(u) = self.updated { if let Some(u) = self.updated {
oprops.set_updated(convert_datetime(u))?; person.set_updated(XsdDateTime::from(convert_datetime(u)));
}
if let Some(i) = &self.preferred_username {
oprops.set_name_xsd_string(i.to_owned())?;
} }
if let Some(avatar_url) = &self.avatar { if let Some(avatar_url) = &self.avatar {
let mut image = Image::new(); let mut image = Image::new();
image image.set_url(avatar_url.to_owned());
.object_props person.set_icon(image.into_any_base()?);
.set_url_xsd_any_uri(avatar_url.to_owned())?;
let any_image = AnyImage::from_concrete(image)?;
oprops.set_icon_any_image(any_image)?;
} }
let mut endpoint_props = EndpointProperties::default(); let mut ap_actor = ApActor::new(self.get_inbox_url().parse()?, person);
ap_actor
.set_outbox(self.get_outbox_url().parse()?)
.set_followers(self.get_followers_url().parse()?)
.set_following(self.get_following_url().parse()?)
.set_liked(self.get_liked_url().parse()?)
.set_endpoints(Endpoints {
shared_inbox: Some(self.get_shared_inbox_url().parse()?),
..Default::default()
});
endpoint_props.set_shared_inbox(self.get_shared_inbox_url())?; if let Some(i) = &self.preferred_username {
ap_actor.set_preferred_username(i.to_owned());
}
let mut actor_props = ApActorProperties::default(); Ok(Ext1::new(ap_actor, self.get_public_key_ext()))
actor_props
.set_inbox(self.get_inbox_url())?
.set_outbox(self.get_outbox_url())?
.set_endpoints(endpoint_props)?
.set_followers(self.get_followers_url())?
.set_following(self.get_following_url())?
.set_liked(self.get_liked_url())?;
Ok(Ext2::new(person, actor_props, self.get_public_key_ext()))
} }
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> { fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
unimplemented!() unimplemented!()
@ -203,31 +193,33 @@ impl ActorType for User_ {
impl FromApub for UserForm { impl FromApub for UserForm {
type ApubType = PersonExt; type ApubType = PersonExt;
/// Parse an ActivityPub person received from another instance into a Lemmy user. /// Parse an ActivityPub person received from another instance into a Lemmy user.
async fn from_apub(person: &PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> { async fn from_apub(person: &mut PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> {
let oprops = &person.inner.object_props; let avatar = match person.take_icon() {
let aprops = &person.ext_one; Some(any_image) => Image::from_any_base(any_image.as_one().unwrap().clone())
let public_key: &PublicKey = &person.ext_two.public_key; .unwrap()
.unwrap()
let avatar = match oprops.get_icon_any_image() { .url
Some(any_image) => any_image .unwrap()
.to_owned() .as_single_xsd_any_uri()
.into_concrete::<Image>()?
.object_props
.get_url_xsd_any_uri()
.map(|u| u.to_string()), .map(|u| u.to_string()),
None => None, None => None,
}; };
Ok(UserForm { Ok(UserForm {
name: oprops.get_name_xsd_string().unwrap().to_string(), name: person
preferred_username: aprops.get_preferred_username().map(|u| u.to_string()), .take_name()
.unwrap()
.as_single_xsd_string()
.unwrap()
.into(),
preferred_username: person.inner.take_preferred_username(),
password_encrypted: "".to_string(), password_encrypted: "".to_string(),
admin: false, admin: false,
banned: false, banned: false,
email: None, email: None,
avatar, avatar,
updated: oprops updated: person
.get_updated() .take_updated()
.map(|u| u.as_ref().to_owned().naive_local()), .map(|u| u.as_ref().to_owned().naive_local()),
show_nsfw: false, show_nsfw: false,
theme: "".to_string(), theme: "".to_string(),
@ -237,11 +229,13 @@ impl FromApub for UserForm {
show_avatars: false, show_avatars: false,
send_notifications_to_email: false, send_notifications_to_email: false,
matrix_user_id: None, matrix_user_id: None,
actor_id: oprops.get_id().unwrap().to_string(), actor_id: person.id().unwrap().to_string(),
bio: oprops.get_summary_xsd_string().map(|s| s.to_string()), bio: person
.take_summary()
.map(|s| s.as_single_xsd_string().unwrap().into()),
local: false, local: false,
private_key: None, private_key: None,
public_key: Some(public_key.to_owned().public_key_pem), public_key: Some(person.ext_one.public_key.to_owned().public_key_pem),
last_refreshed_at: Some(naive_now()), last_refreshed_at: Some(naive_now()),
}) })
} }
@ -254,7 +248,7 @@ pub async fn get_apub_user_http(
) -> Result<HttpResponse<Body>, LemmyError> { ) -> Result<HttpResponse<Body>, LemmyError> {
let user_name = info.into_inner().user_name; let user_name = info.into_inner().user_name;
let user = blocking(&db, move |conn| { let user = blocking(&db, move |conn| {
User_::find_by_email_or_username(conn, &user_name) Claims::find_by_email_or_username(conn, &user_name)
}) })
.await??; .await??;
let u = user.to_apub(&db).await?; let u = user.to_apub(&db).await?;

View file

@ -3,19 +3,10 @@ use crate::{
apub::{ apub::{
extensions::signatures::verify, extensions::signatures::verify,
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}, fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
insert_activity,
FromApub, FromApub,
}, },
blocking, blocking,
db::{
activity::insert_activity,
community::{CommunityFollower, CommunityFollowerForm},
private_message::{PrivateMessage, PrivateMessageForm},
private_message_view::PrivateMessageView,
user::User_,
Crud,
Followable,
},
naive_now,
routes::{ChatServerParam, DbPoolParam}, routes::{ChatServerParam, DbPoolParam},
websocket::{server::SendUserRoomMessage, UserOperation}, websocket::{server::SendUserRoomMessage, UserOperation},
DbPool, DbPool,
@ -26,6 +17,15 @@ use activitystreams::{
object::Note, object::Note,
}; };
use actix_web::{client::Client, web, HttpRequest, HttpResponse}; use actix_web::{client::Client, web, HttpRequest, HttpResponse};
use lemmy_db::{
community::{CommunityFollower, CommunityFollowerForm},
naive_now,
private_message::{PrivateMessage, PrivateMessageForm},
private_message_view::PrivateMessageView,
user::User_,
Crud,
Followable,
};
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
use std::fmt::Debug; use std::fmt::Debug;
@ -116,7 +116,7 @@ async fn receive_create_private_message(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let note = create let mut note = create
.create_props .create_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -135,7 +135,7 @@ async fn receive_create_private_message(
insert_activity(user.id, create, false, pool).await?; insert_activity(user.id, create, false, pool).await?;
let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?; let private_message = PrivateMessageForm::from_apub(&mut note, client, pool).await?;
let inserted_private_message = blocking(pool, move |conn| { let inserted_private_message = blocking(pool, move |conn| {
PrivateMessage::create(conn, &private_message) PrivateMessage::create(conn, &private_message)
@ -168,7 +168,7 @@ async fn receive_update_private_message(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let note = update let mut note = update
.update_props .update_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -187,7 +187,7 @@ async fn receive_update_private_message(
insert_activity(user.id, update, false, pool).await?; insert_activity(user.id, update, false, pool).await?;
let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?; let private_message_form = PrivateMessageForm::from_apub(&mut note, client, pool).await?;
let private_message_ap_id = private_message_form.ap_id.clone(); let private_message_ap_id = private_message_form.ap_id.clone();
let private_message = blocking(pool, move |conn| { let private_message = blocking(pool, move |conn| {
@ -228,7 +228,7 @@ async fn receive_delete_private_message(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let note = delete let mut note = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -247,7 +247,7 @@ async fn receive_delete_private_message(
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?; let private_message_form = PrivateMessageForm::from_apub(&mut note, client, pool).await?;
let private_message_ap_id = private_message_form.ap_id; let private_message_ap_id = private_message_form.ap_id;
let private_message = blocking(pool, move |conn| { let private_message = blocking(pool, move |conn| {
@ -308,7 +308,7 @@ async fn receive_undo_delete_private_message(
.to_owned() .to_owned()
.into_concrete::<Delete>()?; .into_concrete::<Delete>()?;
let note = delete let mut note = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -327,7 +327,7 @@ async fn receive_undo_delete_private_message(
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?; let private_message = PrivateMessageForm::from_apub(&mut note, client, pool).await?;
let private_message_ap_id = private_message.ap_id.clone(); let private_message_ap_id = private_message.ap_id.clone();
let private_message_id = blocking(pool, move |conn| { let private_message_id = blocking(pool, move |conn| {

View file

@ -1,18 +1,16 @@
// This is for db migrations that require code // This is for db migrations that require code
use super::{ use crate::LemmyError;
use diesel::*;
use lemmy_db::{
comment::Comment, comment::Comment,
community::{Community, CommunityForm}, community::{Community, CommunityForm},
naive_now,
post::Post, post::Post,
private_message::PrivateMessage, private_message::PrivateMessage,
user::{UserForm, User_}, user::{UserForm, User_},
Crud,
}; };
use crate::{ use lemmy_utils::{generate_actor_keypair, make_apub_endpoint, EndpointType};
apub::{extensions::signatures::generate_actor_keypair, make_apub_endpoint, EndpointType},
db::Crud,
naive_now,
LemmyError,
};
use diesel::*;
use log::info; use log::info;
pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), LemmyError> { pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), LemmyError> {
@ -26,7 +24,7 @@ pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), LemmyError> {
} }
fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> { fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::user_::dsl::*; use lemmy_db::schema::user_::dsl::*;
info!("Running user_updates_2020_04_02"); info!("Running user_updates_2020_04_02");
@ -77,7 +75,7 @@ fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
} }
fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> { fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::community::dsl::*; use lemmy_db::schema::community::dsl::*;
info!("Running community_updates_2020_04_02"); info!("Running community_updates_2020_04_02");
@ -121,7 +119,7 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
} }
fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> { fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::post::dsl::*; use lemmy_db::schema::post::dsl::*;
info!("Running post_updates_2020_04_03"); info!("Running post_updates_2020_04_03");
@ -134,7 +132,8 @@ fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
sql_query("alter table post disable trigger refresh_post").execute(conn)?; sql_query("alter table post disable trigger refresh_post").execute(conn)?;
for cpost in &incorrect_posts { for cpost in &incorrect_posts {
Post::update_ap_id(&conn, cpost.id)?; let apub_id = make_apub_endpoint(EndpointType::Post, &cpost.id.to_string()).to_string();
Post::update_ap_id(&conn, cpost.id, apub_id)?;
} }
info!("{} post rows updated.", incorrect_posts.len()); info!("{} post rows updated.", incorrect_posts.len());
@ -145,7 +144,7 @@ fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
} }
fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> { fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::comment::dsl::*; use lemmy_db::schema::comment::dsl::*;
info!("Running comment_updates_2020_04_03"); info!("Running comment_updates_2020_04_03");
@ -158,7 +157,8 @@ fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
sql_query("alter table comment disable trigger refresh_comment").execute(conn)?; sql_query("alter table comment disable trigger refresh_comment").execute(conn)?;
for ccomment in &incorrect_comments { for ccomment in &incorrect_comments {
Comment::update_ap_id(&conn, ccomment.id)?; let apub_id = make_apub_endpoint(EndpointType::Comment, &ccomment.id.to_string()).to_string();
Comment::update_ap_id(&conn, ccomment.id, apub_id)?;
} }
sql_query("alter table comment enable trigger refresh_comment").execute(conn)?; sql_query("alter table comment enable trigger refresh_comment").execute(conn)?;
@ -169,7 +169,7 @@ fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
} }
fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), LemmyError> { fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::private_message::dsl::*; use lemmy_db::schema::private_message::dsl::*;
info!("Running private_message_updates_2020_05_05"); info!("Running private_message_updates_2020_05_05");
@ -179,14 +179,11 @@ fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), LemmyEr
.filter(local.eq(true)) .filter(local.eq(true))
.load::<PrivateMessage>(conn)?; .load::<PrivateMessage>(conn)?;
sql_query("alter table private_message disable trigger refresh_private_message").execute(conn)?;
for cpm in &incorrect_pms { for cpm in &incorrect_pms {
PrivateMessage::update_ap_id(&conn, cpm.id)?; let apub_id = make_apub_endpoint(EndpointType::PrivateMessage, &cpm.id.to_string()).to_string();
PrivateMessage::update_ap_id(&conn, cpm.id, apub_id)?;
} }
sql_query("alter table private_message enable trigger refresh_private_message").execute(conn)?;
info!("{} private message rows updated.", incorrect_pms.len()); info!("{} private message rows updated.", incorrect_pms.len());
Ok(()) Ok(())

View file

@ -5,76 +5,34 @@ pub extern crate strum_macros;
pub extern crate lazy_static; pub extern crate lazy_static;
#[macro_use] #[macro_use]
pub extern crate failure; pub extern crate failure;
#[macro_use]
pub extern crate diesel;
pub extern crate actix; pub extern crate actix;
pub extern crate actix_web; pub extern crate actix_web;
pub extern crate bcrypt; pub extern crate bcrypt;
pub extern crate chrono; pub extern crate chrono;
pub extern crate comrak; pub extern crate diesel;
pub extern crate dotenv; pub extern crate dotenv;
pub extern crate jsonwebtoken; pub extern crate jsonwebtoken;
pub extern crate lettre;
pub extern crate lettre_email;
extern crate log; extern crate log;
pub extern crate openssl; pub extern crate openssl;
pub extern crate rand;
pub extern crate regex;
pub extern crate rss; pub extern crate rss;
pub extern crate serde; pub extern crate serde;
pub extern crate serde_json; pub extern crate serde_json;
pub extern crate sha2; pub extern crate sha2;
pub extern crate strum; pub extern crate strum;
pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
where
F: FnOnce(&diesel::PgConnection) -> T + Send + 'static,
T: Send + 'static,
{
let pool = pool.clone();
let res = actix_web::web::block(move || {
let conn = pool.get()?;
let res = (f)(&conn);
Ok(res) as Result<_, LemmyError>
})
.await?;
Ok(res)
}
pub mod api; pub mod api;
pub mod apub; pub mod apub;
pub mod db; pub mod code_migrations;
pub mod rate_limit; pub mod rate_limit;
pub mod request; pub mod request;
pub mod routes; pub mod routes;
pub mod schema;
pub mod settings;
pub mod version; pub mod version;
pub mod websocket; pub mod websocket;
use crate::{ use crate::request::{retry, RecvError};
request::{retry, RecvError},
settings::Settings,
};
use actix_web::{client::Client, dev::ConnectionInfo}; use actix_web::{client::Client, dev::ConnectionInfo};
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc};
use itertools::Itertools;
use lettre::{
smtp::{
authentication::{Credentials, Mechanism},
extension::ClientId,
ConnectionReuseParameters,
},
ClientSecurity,
SmtpClient,
Transport,
};
use lettre_email::Email;
use log::error; use log::error;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use regex::{Regex, RegexBuilder};
use serde::Deserialize; use serde::Deserialize;
pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>; pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
@ -89,14 +47,6 @@ pub struct LemmyError {
inner: failure::Error, inner: failure::Error,
} }
impl std::fmt::Display for LemmyError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.inner.fmt(f)
}
}
impl actix_web::error::ResponseError for LemmyError {}
impl<T> From<T> for LemmyError impl<T> From<T> for LemmyError
where where
T: Into<failure::Error>, T: Into<failure::Error>,
@ -106,113 +56,13 @@ where
} }
} }
pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> { impl std::fmt::Display for LemmyError {
DateTime::<Utc>::from_utc(ndt, Utc) fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
} self.inner.fmt(f)
pub fn naive_now() -> NaiveDateTime {
chrono::prelude::Utc::now().naive_utc()
}
pub fn naive_from_unix(time: i64) -> NaiveDateTime {
NaiveDateTime::from_timestamp(time, 0)
}
pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime<FixedOffset> {
let now = Local::now();
DateTime::<FixedOffset>::from_utc(datetime, *now.offset())
}
pub fn is_email_regex(test: &str) -> bool {
EMAIL_REGEX.is_match(test)
}
pub async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyError> {
let response = retry(|| client.get(test).send()).await?;
if response
.headers()
.get("Content-Type")
.ok_or_else(|| format_err!("No Content-Type header"))?
.to_str()?
.starts_with("image/")
{
Ok(())
} else {
Err(format_err!("Not an image type.").into())
} }
} }
pub fn remove_slurs(test: &str) -> String { impl actix_web::error::ResponseError for LemmyError {}
SLUR_REGEX.replace_all(test, "*removed*").to_string()
}
pub fn slur_check(test: &str) -> Result<(), Vec<&str>> {
let mut matches: Vec<&str> = SLUR_REGEX.find_iter(test).map(|mat| mat.as_str()).collect();
// Unique
matches.sort_unstable();
matches.dedup();
if matches.is_empty() {
Ok(())
} else {
Err(matches)
}
}
pub fn slurs_vec_to_str(slurs: Vec<&str>) -> String {
let start = "No slurs - ";
let combined = &slurs.join(", ");
[start, combined].concat()
}
pub fn generate_random_string() -> String {
thread_rng().sample_iter(&Alphanumeric).take(30).collect()
}
pub fn send_email(
subject: &str,
to_email: &str,
to_username: &str,
html: &str,
) -> Result<(), String> {
let email_config = Settings::get().email.ok_or("no_email_setup")?;
let email = Email::builder()
.to((to_email, to_username))
.from(email_config.smtp_from_address.to_owned())
.subject(subject)
.html(html)
.build()
.unwrap();
let mailer = if email_config.use_tls {
SmtpClient::new_simple(&email_config.smtp_server).unwrap()
} else {
SmtpClient::new(&email_config.smtp_server, ClientSecurity::None).unwrap()
}
.hello_name(ClientId::Domain(Settings::get().hostname))
.smtp_utf8(true)
.authentication_mechanism(Mechanism::Plain)
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited);
let mailer = if let (Some(login), Some(password)) =
(&email_config.smtp_login, &email_config.smtp_password)
{
mailer.credentials(Credentials::new(login.to_owned(), password.to_owned()))
} else {
mailer
};
let mut transport = mailer.transport();
let result = transport.send(email.into());
transport.close();
match result {
Ok(_) => Ok(()),
Err(e) => Err(e.to_string()),
}
}
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct IframelyResponse { pub struct IframelyResponse {
@ -319,13 +169,25 @@ async fn fetch_iframely_and_pictrs_data(
} }
} }
pub fn markdown_to_html(text: &str) -> String { pub async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyError> {
comrak::markdown_to_html(text, &comrak::ComrakOptions::default()) let response = retry(|| client.get(test).send()).await?;
if response
.headers()
.get("Content-Type")
.ok_or_else(|| format_err!("No Content-Type header"))?
.to_str()?
.starts_with("image/")
{
Ok(())
} else {
Err(format_err!("Not an image type.").into())
}
} }
pub fn get_ip(conn_info: &ConnectionInfo) -> String { pub fn get_ip(conn_info: &ConnectionInfo) -> String {
conn_info conn_info
.remote_addr() .realip_remote_addr()
.unwrap_or("127.0.0.1:12345") .unwrap_or("127.0.0.1:12345")
.split(':') .split(':')
.next() .next()
@ -333,127 +195,37 @@ pub fn get_ip(conn_info: &ConnectionInfo) -> String {
.to_string() .to_string()
} }
// TODO nothing is done with community / group webfingers yet, so just ignore those for now pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
#[derive(Clone, PartialEq, Eq, Hash)] where
pub struct MentionData { F: FnOnce(&diesel::PgConnection) -> T + Send + 'static,
pub name: String, T: Send + 'static,
pub domain: String, {
} let pool = pool.clone();
let res = actix_web::web::block(move || {
let conn = pool.get()?;
let res = (f)(&conn);
Ok(res) as Result<_, LemmyError>
})
.await?;
impl MentionData { Ok(res)
pub fn is_local(&self) -> bool {
Settings::get().hostname.eq(&self.domain)
}
pub fn full_name(&self) -> String {
format!("@{}@{}", &self.name, &self.domain)
}
}
pub fn scrape_text_for_mentions(text: &str) -> Vec<MentionData> {
let mut out: Vec<MentionData> = Vec::new();
for caps in WEBFINGER_USER_REGEX.captures_iter(text) {
out.push(MentionData {
name: caps["name"].to_string(),
domain: caps["domain"].to_string(),
});
}
out.into_iter().unique().collect()
}
pub fn is_valid_username(name: &str) -> bool {
VALID_USERNAME_REGEX.is_match(name)
}
pub fn is_valid_community_name(name: &str) -> bool {
VALID_COMMUNITY_NAME_REGEX.is_match(name)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::is_image_content_type;
is_email_regex,
is_image_content_type,
is_valid_community_name,
is_valid_username,
remove_slurs,
scrape_text_for_mentions,
slur_check,
slurs_vec_to_str,
};
#[test]
fn test_mentions_regex() {
let text = "Just read a great blog post by [@tedu@honk.teduangst.com](/u/test). And another by !test_community@fish.teduangst.com . Another [@lemmy@lemmy-alpha:8540](/u/fish)";
let mentions = scrape_text_for_mentions(text);
assert_eq!(mentions[0].name, "tedu".to_string());
assert_eq!(mentions[0].domain, "honk.teduangst.com".to_string());
assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string());
}
#[test] #[test]
fn test_image() { fn test_image() {
actix_rt::System::new("tset_image").block_on(async move { actix_rt::System::new("tset_image").block_on(async move {
let client = actix_web::client::Client::default(); let client = actix_web::client::Client::default();
assert!(is_image_content_type(&client, "https://1734811051.rsc.cdn77.org/data/images/full/365645/as-virus-kills-navajos-in-their-homes-tribal-women-provide-lifeline.jpg?w=600?w=650").await.is_ok()); assert!(is_image_content_type(&client, "https://1734811051.rsc.cdn77.org/data/images/full/365645/as-virus-kills-navajos-in-their-homes-tribal-women-provide-lifeline.jpg?w=600?w=650").await.is_ok());
assert!(is_image_content_type(&client, assert!(is_image_content_type(&client,
"https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20" "https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20"
) )
.await.is_err() .await.is_err()
); );
}); });
}
#[test]
fn test_email() {
assert!(is_email_regex("gush@gmail.com"));
assert!(!is_email_regex("nada_neutho"));
}
#[test]
fn test_valid_register_username() {
assert!(is_valid_username("Hello_98"));
assert!(is_valid_username("ten"));
assert!(!is_valid_username("Hello-98"));
assert!(!is_valid_username("a"));
assert!(!is_valid_username(""));
}
#[test]
fn test_valid_community_name() {
assert!(is_valid_community_name("example"));
assert!(is_valid_community_name("example_community"));
assert!(!is_valid_community_name("Example"));
assert!(!is_valid_community_name("Ex"));
assert!(!is_valid_community_name(""));
}
#[test]
fn test_slur_filter() {
let test =
"coons test dindu ladyboy tranny retardeds. Capitalized Niggerz. This is a bunch of other safe text.";
let slur_free = "No slurs here";
assert_eq!(
remove_slurs(&test),
"*removed* test *removed* *removed* *removed* *removed*. Capitalized *removed*. This is a bunch of other safe text."
.to_string()
);
let has_slurs_vec = vec![
"Niggerz",
"coons",
"dindu",
"ladyboy",
"retardeds",
"tranny",
];
let has_slurs_err_str = "No slurs - Niggerz, coons, dindu, ladyboy, retardeds, tranny";
assert_eq!(slur_check(test), Err(has_slurs_vec));
assert_eq!(slur_check(slur_free), Ok(()));
if let Err(slur_vec) = slur_check(test) {
assert_eq!(&slurs_vec_to_str(slur_vec), has_slurs_err_str);
}
} }
// These helped with testing // These helped with testing
@ -470,21 +242,4 @@ mod tests {
// let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu"); // let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu");
// assert!(res_other.is_err()); // assert!(res_other.is_err());
// } // }
// #[test]
// fn test_send_email() {
// let result = send_email("not a subject", "test_email@gmail.com", "ur user", "<h1>HI there</h1>");
// assert!(result.is_ok());
// }
}
lazy_static! {
static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
static ref SLUR_REGEX: Regex = RegexBuilder::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|\bn(i|1)g(\b|g?(a|er)?(s|z)?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btr(a|@)nn?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap();
static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
// TODO keep this old one, it didn't work with port well tho
// static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)").unwrap();
static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._:-]+)").unwrap();
static ref VALID_USERNAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]{3,20}$").unwrap();
static ref VALID_COMMUNITY_NAME_REGEX: Regex = Regex::new(r"^[a-z0-9_]{3,20}$").unwrap();
} }

View file

@ -22,22 +22,20 @@ use diesel::{
r2d2::{ConnectionManager, Pool}, r2d2::{ConnectionManager, Pool},
PgConnection, PgConnection,
}; };
use lemmy_db::get_database_url_from_env;
use lemmy_server::{ use lemmy_server::{
blocking, blocking,
db::code_migrations::run_advanced_migrations, code_migrations::run_advanced_migrations,
rate_limit::{rate_limiter::RateLimiter, RateLimit}, rate_limit::{rate_limiter::RateLimiter, RateLimit},
routes::{api, federation, feeds, index, nodeinfo, webfinger}, routes::{api, federation, feeds, index, nodeinfo, webfinger},
settings::Settings,
websocket::server::*, websocket::server::*,
LemmyError, LemmyError,
}; };
use regex::Regex; use lemmy_utils::{settings::Settings, CACHE_CONTROL_REGEX};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
lazy_static! { lazy_static! {
static ref CACHE_CONTROL_REGEX: Regex =
Regex::new("^((text|image)/.+|application/javascript)$").unwrap();
// static ref CACHE_CONTROL_VALUE: String = format!("public, max-age={}", 365 * 24 * 60 * 60); // static ref CACHE_CONTROL_VALUE: String = format!("public, max-age={}", 365 * 24 * 60 * 60);
// Test out 1 hour here, this is breaking some things // Test out 1 hour here, this is breaking some things
static ref CACHE_CONTROL_VALUE: String = format!("public, max-age={}", 60 * 60); static ref CACHE_CONTROL_VALUE: String = format!("public, max-age={}", 60 * 60);
@ -51,11 +49,15 @@ async fn main() -> Result<(), LemmyError> {
let settings = Settings::get(); let settings = Settings::get();
// Set up the r2d2 connection pool // Set up the r2d2 connection pool
let manager = ConnectionManager::<PgConnection>::new(&settings.get_database_url()); let db_url = match get_database_url_from_env() {
Ok(url) => url,
Err(_) => settings.get_database_url(),
};
let manager = ConnectionManager::<PgConnection>::new(&db_url);
let pool = Pool::builder() let pool = Pool::builder()
.max_size(settings.database.pool_size) .max_size(settings.database.pool_size)
.build(manager) .build(manager)
.unwrap_or_else(|_| panic!("Error connecting to {}", settings.get_database_url())); .unwrap_or_else(|_| panic!("Error connecting to {}", db_url));
// Run the migrations from code // Run the migrations from code
blocking(&pool, move |conn| { blocking(&pool, move |conn| {

View file

@ -1,7 +1,8 @@
use super::{IPAddr, Settings}; use super::IPAddr;
use crate::{get_ip, settings::RateLimitConfig, LemmyError}; use crate::{get_ip, LemmyError};
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
use futures::future::{ok, Ready}; use futures::future::{ok, Ready};
use lemmy_utils::settings::{RateLimitConfig, Settings};
use rate_limiter::{RateLimitType, RateLimiter}; use rate_limiter::{RateLimitType, RateLimiter};
use std::{ use std::{
future::Future, future::Future,

View file

@ -1,17 +1,15 @@
use crate::{ use crate::apub::{
apub::{ comment::get_apub_comment,
comment::get_apub_comment, community::*,
community::*, community_inbox::community_inbox,
community_inbox::community_inbox, post::get_apub_post,
post::get_apub_post, shared_inbox::shared_inbox,
shared_inbox::shared_inbox, user::*,
user::*, user_inbox::user_inbox,
user_inbox::user_inbox, APUB_JSON_CONTENT_TYPE,
APUB_JSON_CONTENT_TYPE,
},
settings::Settings,
}; };
use actix_web::*; use actix_web::*;
use lemmy_utils::settings::Settings;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
if Settings::get().federation.enabled { if Settings::get().federation.enabled {

View file

@ -1,26 +1,21 @@
use crate::{ use crate::{api::claims::Claims, blocking, routes::DbPoolParam, LemmyError};
blocking,
db::{
comment_view::{ReplyQueryBuilder, ReplyView},
community::Community,
post_view::{PostQueryBuilder, PostView},
site_view::SiteView,
user::{Claims, User_},
user_mention_view::{UserMentionQueryBuilder, UserMentionView},
ListingType,
SortType,
},
markdown_to_html,
routes::DbPoolParam,
settings::Settings,
LemmyError,
};
use actix_web::{error::ErrorBadRequest, *}; use actix_web::{error::ErrorBadRequest, *};
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
use diesel::{ use diesel::{
r2d2::{ConnectionManager, Pool}, r2d2::{ConnectionManager, Pool},
PgConnection, PgConnection,
}; };
use lemmy_db::{
comment_view::{ReplyQueryBuilder, ReplyView},
community::Community,
post_view::{PostQueryBuilder, PostView},
site_view::SiteView,
user::User_,
user_mention_view::{UserMentionQueryBuilder, UserMentionView},
ListingType,
SortType,
};
use lemmy_utils::{markdown_to_html, settings::Settings};
use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder}; use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
use serde::Deserialize; use serde::Deserialize;
use std::str::FromStr; use std::str::FromStr;
@ -131,7 +126,7 @@ fn get_feed_user(
) -> Result<ChannelBuilder, LemmyError> { ) -> Result<ChannelBuilder, LemmyError> {
let site_view = SiteView::read(&conn)?; let site_view = SiteView::read(&conn)?;
let user = User_::find_by_username(&conn, &user_name)?; let user = User_::find_by_username(&conn, &user_name)?;
let user_url = user.get_profile_url(); let user_url = user.get_profile_url(&Settings::get().hostname);
let posts = PostQueryBuilder::create(&conn) let posts = PostQueryBuilder::create(&conn)
.listing_type(ListingType::All) .listing_type(ListingType::All)

View file

@ -1,6 +1,6 @@
use crate::settings::Settings;
use actix_files::NamedFile; use actix_files::NamedFile;
use actix_web::*; use actix_web::*;
use lemmy_utils::settings::Settings;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg cfg

View file

@ -1,13 +1,7 @@
use crate::{ use crate::{blocking, routes::DbPoolParam, version, LemmyError};
apub::get_apub_protocol_string,
blocking,
db::site_view::SiteView,
routes::DbPoolParam,
version,
LemmyError,
Settings,
};
use actix_web::{body::Body, error::ErrorBadRequest, *}; use actix_web::{body::Body, error::ErrorBadRequest, *};
use lemmy_db::site_view::SiteView;
use lemmy_utils::{get_apub_protocol_string, settings::Settings};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;

View file

@ -1,12 +1,7 @@
use crate::{ use crate::{blocking, routes::DbPoolParam, LemmyError};
blocking,
db::{community::Community, user::User_},
routes::DbPoolParam,
LemmyError,
Settings,
};
use actix_web::{error::ErrorBadRequest, web::Query, *}; use actix_web::{error::ErrorBadRequest, web::Query, *};
use regex::Regex; use lemmy_db::{community::Community, user::User_};
use lemmy_utils::{settings::Settings, WEBFINGER_COMMUNITY_REGEX, WEBFINGER_USER_REGEX};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Deserialize)] #[derive(Deserialize)]
@ -40,19 +35,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
} }
} }
lazy_static! {
static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!(
"^group:([a-z0-9_]{{3, 20}})@{}$",
Settings::get().hostname
))
.unwrap();
static ref WEBFINGER_USER_REGEX: Regex = Regex::new(&format!(
"^acct:([a-z0-9_]{{3, 20}})@{}$",
Settings::get().hostname
))
.unwrap();
}
/// Responds to webfinger requests of the following format. There isn't any real documentation for /// Responds to webfinger requests of the following format. There isn't any real documentation for
/// this, but it described in this blog post: /// this, but it described in this blog post:
/// https://mastodon.social/.well-known/webfinger?resource=acct:gargron@mastodon.social /// https://mastodon.social/.well-known/webfinger?resource=acct:gargron@mastodon.social

View file

@ -1 +1 @@
pub const VERSION: &str = "v0.7.8"; pub const VERSION: &str = "v0.7.16";

5
server/test.sh vendored Executable file
View file

@ -0,0 +1,5 @@
#!/bin/sh
export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
diesel migration run
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
RUST_TEST_THREADS=1 cargo test --workspace

1
ui/assets/css/choices.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -249,3 +249,25 @@ pre {
white-space: pre-wrap; white-space: pre-wrap;
word-break: keep-all; word-break: keep-all;
} }
.form-control.search-input {
float: right !important;
transition: width 0.2s ease-out 0s !important;
}
.show-input {
width: 13em !important;
}
.hide-input {
background: transparent !important;
width: 0px !important;
padding: 0 !important;
}
br.big {
display: block;
content: "";
margin-top: 1rem;
}

File diff suppressed because one or more lines are too long

8
ui/fuse.js vendored
View file

@ -6,12 +6,10 @@ const {
WebIndexPlugin, WebIndexPlugin,
QuantumPlugin, QuantumPlugin,
} = require('fuse-box'); } = require('fuse-box');
// const transformInferno = require('../../dist').default
const transformInferno = require('ts-transform-inferno').default; const transformInferno = require('ts-transform-inferno').default;
const transformClasscat = require('ts-transform-classcat').default; const transformClasscat = require('ts-transform-classcat').default;
let fuse, app; let fuse, app;
let isProduction = false; let isProduction = false;
// var setVersion = require('./set_version.js').setVersion;
Sparky.task('config', _ => { Sparky.task('config', _ => {
fuse = new FuseBox({ fuse = new FuseBox({
@ -45,18 +43,18 @@ Sparky.task('config', _ => {
}); });
app = fuse.bundle('app').instructions('>index.tsx'); app = fuse.bundle('app').instructions('>index.tsx');
}); });
// Sparky.task('version', _ => setVersion());
Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/')); Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
Sparky.task('env', _ => (isProduction = true)); Sparky.task('env', _ => (isProduction = true));
Sparky.task('copy-assets', () => Sparky.task('copy-assets', () =>
Sparky.src('assets/**/**.*').dest(isProduction ? 'dist/' : 'dist/static') Sparky.src('assets/**/**.*').dest(isProduction ? 'dist/' : 'dist/static')
); );
Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => { Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => {
fuse.dev(); fuse.dev({
fallback: 'index.html',
});
app.hmr().watch(); app.hmr().watch();
return fuse.run(); return fuse.run();
}); });
Sparky.task('prod', ['clean', 'env', 'config', 'copy-assets'], _ => { Sparky.task('prod', ['clean', 'env', 'config', 'copy-assets'], _ => {
// fuse.dev({ reload: true }); // remove after demo
return fuse.run(); return fuse.run();
}); });

6
ui/package.json vendored
View file

@ -15,7 +15,6 @@
}, },
"keywords": [], "keywords": [],
"dependencies": { "dependencies": {
"@joeattardi/emoji-button": "^2.12.1",
"@types/autosize": "^3.0.6", "@types/autosize": "^3.0.6",
"@types/js-cookie": "^2.2.6", "@types/js-cookie": "^2.2.6",
"@types/jwt-decode": "^2.2.1", "@types/jwt-decode": "^2.2.1",
@ -24,6 +23,7 @@
"@types/node": "^13.11.1", "@types/node": "^13.11.1",
"autosize": "^4.0.2", "autosize": "^4.0.2",
"bootswatch": "^4.3.1", "bootswatch": "^4.3.1",
"choices.js": "^9.0.1",
"classcat": "^4.0.2", "classcat": "^4.0.2",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"emoji-short-name": "^1.0.0", "emoji-short-name": "^1.0.0",
@ -37,7 +37,6 @@
"markdown-it": "^10.0.0", "markdown-it": "^10.0.0",
"markdown-it-container": "^2.0.0", "markdown-it-container": "^2.0.0",
"markdown-it-emoji": "^1.4.0", "markdown-it-emoji": "^1.4.0",
"mobius1-selectr": "^2.4.13",
"moment": "^2.24.0", "moment": "^2.24.0",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
"prettier": "^2.0.4", "prettier": "^2.0.4",
@ -47,7 +46,6 @@
"tippy.js": "^6.1.1", "tippy.js": "^6.1.1",
"toastify-js": "^1.7.0", "toastify-js": "^1.7.0",
"tributejs": "^5.1.3", "tributejs": "^5.1.3",
"twemoji": "^12.1.2",
"ws": "^7.2.3" "ws": "^7.2.3"
}, },
"devDependencies": { "devDependencies": {
@ -72,7 +70,7 @@
"engineStrict": true, "engineStrict": true,
"husky": { "husky": {
"hooks": { "hooks": {
"pre-commit": "cargo clippy --manifest-path ../server/Cargo.toml --all-targets --all-features -- -D warnings && lint-staged" "pre-commit": "cargo clippy --manifest-path ../server/Cargo.toml --all-targets --workspace -- -D warnings && lint-staged"
} }
}, },
"lint-staged": { "lint-staged": {

25
ui/src/components/cake-day.tsx vendored Normal file
View file

@ -0,0 +1,25 @@
import { Component } from 'inferno';
import { i18n } from '../i18next';
interface CakeDayProps {
creatorName: string;
}
export class CakeDay extends Component<CakeDayProps, any> {
render() {
return (
<div
className={`mx-2 d-inline-block unselectable pointer`}
data-tippy-content={this.cakeDayTippy()}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-cake"></use>
</svg>
</div>
);
}
cakeDayTippy(): string {
return i18n.t('cake_day_info', { creator_name: this.props.creatorName });
}
}

View file

@ -1,4 +1,5 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { Prompt } from 'inferno-router'; import { Prompt } from 'inferno-router';
@ -17,7 +18,6 @@ import {
toast, toast,
setupTribute, setupTribute,
wsJsonToRes, wsJsonToRes,
emojiPicker,
pictrsDeleteToast, pictrsDeleteToast,
} from '../utils'; } from '../utils';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
@ -25,6 +25,7 @@ import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js'; import Tribute from 'tributejs/src/Tribute.js';
import emojiShortName from 'emoji-short-name'; import emojiShortName from 'emoji-short-name';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
interface CommentFormProps { interface CommentFormProps {
postId?: number; postId?: number;
@ -72,7 +73,6 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
super(props, context); super(props, context);
this.tribute = setupTribute(); this.tribute = setupTribute();
this.setupEmojiPicker();
this.state = this.emptyState; this.state = this.emptyState;
@ -98,18 +98,45 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
} }
componentDidMount() { componentDidMount() {
var textarea: any = document.getElementById(this.id); let textarea: any = document.getElementById(this.id);
autosize(textarea); if (textarea) {
this.tribute.attach(textarea); autosize(textarea);
textarea.addEventListener('tribute-replaced', () => { this.tribute.attach(textarea);
this.state.commentForm.content = textarea.value; textarea.addEventListener('tribute-replaced', () => {
this.setState(this.state); this.state.commentForm.content = textarea.value;
autosize.update(textarea); this.setState(this.state);
}); autosize.update(textarea);
});
// Quoting of selected text
let selectedText = window.getSelection().toString();
if (selectedText) {
let quotedText =
selectedText
.split('\n')
.map(t => `> ${t}`)
.join('\n') + '\n\n';
this.state.commentForm.content = quotedText;
this.setState(this.state);
// Not sure why this needs a delay
setTimeout(() => autosize.update(textarea), 10);
}
textarea.focus();
}
}
componentDidUpdate() {
if (this.state.commentForm.content) {
window.onbeforeunload = () => true;
} else {
window.onbeforeunload = undefined;
}
} }
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
window.onbeforeunload = null;
} }
render() { render() {
@ -119,133 +146,123 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
when={this.state.commentForm.content} when={this.state.commentForm.content}
message={i18n.t('block_leaving')} message={i18n.t('block_leaving')}
/> />
<form {UserService.Instance.user ? (
id={this.formId} <form
onSubmit={linkEvent(this, this.handleCommentSubmit)} id={this.formId}
> onSubmit={linkEvent(this, this.handleCommentSubmit)}
<div class="form-group row"> >
<div className={`col-sm-12`}> <div class="form-group row">
<textarea <div className={`col-sm-12`}>
id={this.id} <textarea
className={`form-control ${this.state.previewMode && 'd-none'}`} id={this.id}
value={this.state.commentForm.content} className={`form-control ${
onInput={linkEvent(this, this.handleCommentContentChange)} this.state.previewMode && 'd-none'
onPaste={linkEvent(this, this.handleImageUploadPaste)} }`}
required value={this.state.commentForm.content}
disabled={this.props.disabled} onInput={linkEvent(this, this.handleCommentContentChange)}
rows={2} onPaste={linkEvent(this, this.handleImageUploadPaste)}
maxLength={10000} required
/> disabled={this.props.disabled}
{this.state.previewMode && ( rows={2}
<div maxLength={10000}
className="card card-body md-div"
dangerouslySetInnerHTML={mdToHtml(
this.state.commentForm.content
)}
/> />
)} {this.state.previewMode && (
<div
className="card card-body md-div"
dangerouslySetInnerHTML={mdToHtml(
this.state.commentForm.content
)}
/>
)}
</div>
</div> </div>
</div> <div class="row">
<div class="row"> <div class="col-sm-12">
<div class="col-sm-12"> <button
<button type="submit"
type="submit" class="btn btn-sm btn-secondary mr-2"
class="btn btn-sm btn-secondary mr-2" disabled={this.props.disabled || this.state.loading}
disabled={this.props.disabled || this.state.loading} >
> {this.state.loading ? (
{this.state.loading ? ( <svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
<span>{this.state.buttonTitle}</span>
)}
</button>
{this.state.commentForm.content && (
<button
className={`btn btn-sm mr-2 btn-secondary ${
this.state.previewMode && 'active'
}`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
{i18n.t('preview')}
</button>
)}
{this.props.node && (
<button
type="button"
class="btn btn-sm btn-secondary mr-2"
onClick={linkEvent(this, this.handleReplyCancel)}
>
{i18n.t('cancel')}
</button>
)}
<a
href={markdownHelpUrl}
target="_blank"
class="d-inline-block float-right text-muted font-weight-bold"
title={i18n.t('formatting_help')}
rel="noopener"
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-help-circle"></use>
</svg>
</a>
<form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
<label
htmlFor={`file-upload-${this.id}`}
className={`${UserService.Instance.user && 'pointer'}`}
data-tippy-content={i18n.t('upload_image')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-image"></use>
</svg>
</label>
<input
id={`file-upload-${this.id}`}
type="file"
accept="image/*,video/*"
name="file"
class="d-none"
disabled={!UserService.Instance.user}
onChange={linkEvent(this, this.handleImageUpload)}
/>
</form>
{this.state.imageLoading && (
<svg class="icon icon-spinner spin"> <svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use> <use xlinkHref="#icon-spinner"></use>
</svg> </svg>
) : (
<span>{this.state.buttonTitle}</span>
)} )}
</button> </div>
{this.state.commentForm.content && (
<button
className={`btn btn-sm mr-2 btn-secondary ${
this.state.previewMode && 'active'
}`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
{i18n.t('preview')}
</button>
)}
{this.props.node && (
<button
type="button"
class="btn btn-sm btn-secondary mr-2"
onClick={linkEvent(this, this.handleReplyCancel)}
>
{i18n.t('cancel')}
</button>
)}
<a
href={markdownHelpUrl}
target="_blank"
class="d-inline-block float-right text-muted font-weight-bold"
title={i18n.t('formatting_help')}
rel="noopener"
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-help-circle"></use>
</svg>
</a>
<form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
<label
htmlFor={`file-upload-${this.id}`}
className={`${UserService.Instance.user && 'pointer'}`}
data-tippy-content={i18n.t('upload_image')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-image"></use>
</svg>
</label>
<input
id={`file-upload-${this.id}`}
type="file"
accept="image/*,video/*"
name="file"
class="d-none"
disabled={!UserService.Instance.user}
onChange={linkEvent(this, this.handleImageUpload)}
/>
</form>
{this.state.imageLoading && (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
)}
<span
onClick={linkEvent(this, this.handleEmojiPickerClick)}
class="pointer unselectable d-inline-block mr-3 float-right text-muted font-weight-bold"
data-tippy-content={i18n.t('emoji_picker')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-smile"></use>
</svg>
</span>
</div> </div>
</form>
) : (
<div class="alert alert-warning" role="alert">
<svg class="icon icon-inline mr-2">
<use xlinkHref="#icon-alert-triangle"></use>
</svg>
<T i18nKey="must_login" class="d-inline">
#<Link to="/login">#</Link>
</T>
</div> </div>
</form> )}
</div> </div>
); );
} }
setupEmojiPicker() {
emojiPicker.on('emoji', twemojiHtmlStr => {
if (this.state.commentForm.content == null) {
this.state.commentForm.content = '';
}
var el = document.createElement('div');
el.innerHTML = twemojiHtmlStr;
let nativeUnicode = (el.childNodes[0] as HTMLElement).getAttribute('alt');
let shortName = `:${emojiShortName[nativeUnicode]}:`;
this.state.commentForm.content += shortName;
this.setState(this.state);
});
}
handleFinished(op: UserOperation, data: CommentResponse) { handleFinished(op: UserOperation, data: CommentResponse) {
let isReply = let isReply =
this.props.node !== undefined && data.comment.parent_id !== null; this.props.node !== undefined && data.comment.parent_id !== null;
@ -293,10 +310,6 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
i.setState(i.state); i.setState(i.state);
} }
handleEmojiPickerClick(_i: CommentForm, event: any) {
emojiPicker.togglePicker(event.target);
}
handleCommentContentChange(i: CommentForm, event: any) { handleCommentContentChange(i: CommentForm, event: any) {
i.state.commentForm.content = event.target.value; i.state.commentForm.content = event.target.value;
i.setState(i.state); i.setState(i.state);

View file

@ -73,6 +73,7 @@ interface CommentNodeProps {
showCommunity?: boolean; showCommunity?: boolean;
sort?: CommentSortType; sort?: CommentSortType;
sortType?: SortType; sortType?: SortType;
enableDownvotes: boolean;
} }
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
@ -157,9 +158,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
id: node.comment.creator_id, id: node.comment.creator_id,
local: node.comment.creator_local, local: node.comment.creator_local,
actor_id: node.comment.creator_actor_id, actor_id: node.comment.creator_actor_id,
published: node.comment.creator_published,
}} }}
/> />
</span> </span>
{this.isMod && ( {this.isMod && (
<div className="badge badge-light d-none d-sm-inline mr-2"> <div className="badge badge-light d-none d-sm-inline mr-2">
{i18n.t('mod')} {i18n.t('mod')}
@ -188,8 +191,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</Link> </Link>
</> </>
)} )}
<div <button
className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" class="btn btn-sm text-muted"
onClick={linkEvent(this, this.handleCommentCollapse)} onClick={linkEvent(this, this.handleCommentCollapse)}
> >
{this.state.collapsed ? ( {this.state.collapsed ? (
@ -201,9 +204,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<use xlinkHref="#icon-minus-square"></use> <use xlinkHref="#icon-minus-square"></use>
</svg> </svg>
)} )}
</div> </button>
<span {/* This is an expanding spacer for mobile */}
className={`unselectable pointer ${this.scoreColor}`} <div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
<button
className={`btn btn-sm p-0 unselectable pointer ${this.scoreColor}`}
onClick={linkEvent(node, this.handleCommentUpvote)} onClick={linkEvent(node, this.handleCommentUpvote)}
data-tippy-content={this.pointsTippy} data-tippy-content={this.pointsTippy}
> >
@ -211,7 +216,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<use xlinkHref="#icon-zap"></use> <use xlinkHref="#icon-zap"></use>
</svg> </svg>
<span class="mr-1">{this.state.score}</span> <span class="mr-1">{this.state.score}</span>
</span> </button>
<span className="mr-1"></span> <span className="mr-1"></span>
<span> <span>
<MomentTime data={node.comment} /> <MomentTime data={node.comment} />
@ -279,7 +284,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<span class="ml-1">{this.state.upvotes}</span> <span class="ml-1">{this.state.upvotes}</span>
)} )}
</button> </button>
{WebSocketService.Instance.site.enable_downvotes && ( {this.props.enableDownvotes && (
<button <button
className={`btn btn-link btn-animate ${ className={`btn btn-link btn-animate ${
this.state.my_vote == -1 this.state.my_vote == -1
@ -703,6 +708,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
postCreatorId={this.props.postCreatorId} postCreatorId={this.props.postCreatorId}
sort={this.props.sort} sort={this.props.sort}
sortType={this.props.sortType} sortType={this.props.sortType}
enableDownvotes={this.props.enableDownvotes}
/> />
)} )}
{/* A collapsed clearfix */} {/* A collapsed clearfix */}

View file

@ -24,6 +24,7 @@ interface CommentNodesProps {
showCommunity?: boolean; showCommunity?: boolean;
sort?: CommentSortType; sort?: CommentSortType;
sortType?: SortType; sortType?: SortType;
enableDownvotes: boolean;
} }
export class CommentNodes extends Component< export class CommentNodes extends Component<
@ -52,6 +53,7 @@ export class CommentNodes extends Component<
showCommunity={this.props.showCommunity} showCommunity={this.props.showCommunity}
sort={this.props.sort} sort={this.props.sort}
sortType={this.props.sortType} sortType={this.props.sortType}
enableDownvotes={this.props.enableDownvotes}
/> />
))} ))}
</div> </div>

View file

@ -1,5 +1,4 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { import {
@ -11,6 +10,7 @@ import {
ListCommunitiesForm, ListCommunitiesForm,
SortType, SortType,
WebSocketJsonResponse, WebSocketJsonResponse,
GetSiteResponse,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { wsJsonToRes, toast } from '../utils'; import { wsJsonToRes, toast } from '../utils';
@ -47,6 +47,7 @@ export class Communities extends Component<any, CommunitiesState> {
); );
this.refetch(); this.refetch();
WebSocketService.Instance.getSite();
} }
getPageFromProps(props: any): number { getPageFromProps(props: any): number {
@ -57,12 +58,6 @@ export class Communities extends Component<any, CommunitiesState> {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
componentDidMount() {
document.title = `${i18n.t('communities')} - ${
WebSocketService.Instance.site.name
}`;
}
// Necessary for back button for some reason // Necessary for back button for some reason
componentWillReceiveProps(nextProps: any) { componentWillReceiveProps(nextProps: any) {
if (nextProps.history.action == 'POP') { if (nextProps.history.action == 'POP') {
@ -165,7 +160,7 @@ export class Communities extends Component<any, CommunitiesState> {
</button> </button>
)} )}
{this.state.communities.length == communityLimit && ( {this.state.communities.length > 0 && (
<button <button
class="btn btn-sm btn-secondary" class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)} onClick={linkEvent(this, this.nextPage)}
@ -244,6 +239,9 @@ export class Communities extends Component<any, CommunitiesState> {
found.subscribed = data.community.subscribed; found.subscribed = data.community.subscribed;
found.number_of_subscribers = data.community.number_of_subscribers; found.number_of_subscribers = data.community.number_of_subscribers;
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
document.title = `${i18n.t('communities')} - ${data.site.name}`;
} }
} }
} }

View file

@ -8,7 +8,6 @@ import {
Category, Category,
ListCategoriesResponse, ListCategoriesResponse,
CommunityResponse, CommunityResponse,
GetSiteResponse,
WebSocketJsonResponse, WebSocketJsonResponse,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
@ -30,13 +29,13 @@ interface CommunityFormProps {
onCancel?(): any; onCancel?(): any;
onCreate?(community: Community): any; onCreate?(community: Community): any;
onEdit?(community: Community): any; onEdit?(community: Community): any;
enableNsfw: boolean;
} }
interface CommunityFormState { interface CommunityFormState {
communityForm: CommunityFormI; communityForm: CommunityFormI;
categories: Array<Category>; categories: Array<Category>;
loading: boolean; loading: boolean;
enable_nsfw: boolean;
} }
export class CommunityForm extends Component< export class CommunityForm extends Component<
@ -56,7 +55,6 @@ export class CommunityForm extends Component<
}, },
categories: [], categories: [],
loading: false, loading: false,
enable_nsfw: null,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -86,7 +84,6 @@ export class CommunityForm extends Component<
); );
WebSocketService.Instance.listCategories(); WebSocketService.Instance.listCategories();
WebSocketService.Instance.getSite();
} }
componentDidMount() { componentDidMount() {
@ -100,8 +97,22 @@ export class CommunityForm extends Component<
}); });
} }
componentDidUpdate() {
if (
!this.state.loading &&
(this.state.communityForm.name ||
this.state.communityForm.title ||
this.state.communityForm.description)
) {
window.onbeforeunload = () => true;
} else {
window.onbeforeunload = undefined;
}
}
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
window.onbeforeunload = null;
} }
render() { render() {
@ -187,7 +198,7 @@ export class CommunityForm extends Component<
</div> </div>
</div> </div>
{this.state.enable_nsfw && ( {this.props.enableNsfw && (
<div class="form-group row"> <div class="form-group row">
<div class="col-12"> <div class="col-12">
<div class="form-check"> <div class="form-check">
@ -303,10 +314,6 @@ export class CommunityForm extends Component<
let data = res.data as CommunityResponse; let data = res.data as CommunityResponse;
this.state.loading = false; this.state.loading = false;
this.props.onEdit(data.community); this.props.onEdit(data.community);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.enable_nsfw = data.site.enable_nsfw;
this.setState(this.state);
} }
} }
} }

View file

@ -23,6 +23,8 @@ import {
GetCommentsResponse, GetCommentsResponse,
CommentResponse, CommentResponse,
WebSocketJsonResponse, WebSocketJsonResponse,
GetSiteResponse,
Site,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { PostListings } from './post-listings'; import { PostListings } from './post-listings';
@ -60,6 +62,7 @@ interface State {
dataType: DataType; dataType: DataType;
sort: SortType; sort: SortType;
page: number; page: number;
site: Site;
} }
export class Community extends Component<any, State> { export class Community extends Component<any, State> {
@ -97,6 +100,20 @@ export class Community extends Component<any, State> {
dataType: getDataTypeFromProps(this.props), dataType: getDataTypeFromProps(this.props),
sort: getSortTypeFromProps(this.props), sort: getSortTypeFromProps(this.props),
page: getPageFromProps(this.props), page: getPageFromProps(this.props),
site: {
id: undefined,
name: undefined,
creator_id: undefined,
published: undefined,
creator_name: undefined,
number_of_users: undefined,
number_of_posts: undefined,
number_of_comments: undefined,
number_of_communities: undefined,
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
},
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -119,6 +136,7 @@ export class Community extends Component<any, State> {
name: this.state.communityName ? this.state.communityName : null, name: this.state.communityName ? this.state.communityName : null,
}; };
WebSocketService.Instance.getCommunity(form); WebSocketService.Instance.getCommunity(form);
WebSocketService.Instance.getSite();
} }
componentWillUnmount() { componentWillUnmount() {
@ -174,6 +192,7 @@ export class Community extends Component<any, State> {
moderators={this.state.moderators} moderators={this.state.moderators}
admins={this.state.admins} admins={this.state.admins}
online={this.state.online} online={this.state.online}
enableNsfw={this.state.site.enable_nsfw}
/> />
</div> </div>
</div> </div>
@ -188,6 +207,8 @@ export class Community extends Component<any, State> {
posts={this.state.posts} posts={this.state.posts}
removeDuplicates removeDuplicates
sort={this.state.sort} sort={this.state.sort}
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
/> />
) : ( ) : (
<CommentNodes <CommentNodes
@ -195,6 +216,7 @@ export class Community extends Component<any, State> {
noIndent noIndent
sortType={this.state.sort} sortType={this.state.sort}
showContext showContext
enableDownvotes={this.state.site.enable_downvotes}
/> />
); );
} }
@ -238,7 +260,7 @@ export class Community extends Component<any, State> {
{i18n.t('prev')} {i18n.t('prev')}
</button> </button>
)} )}
{this.state.posts.length == fetchLimit && ( {this.state.posts.length > 0 && (
<button <button
class="btn btn-sm btn-secondary" class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)} onClick={linkEvent(this, this.nextPage)}
@ -331,7 +353,7 @@ export class Community extends Component<any, State> {
this.state.moderators = data.moderators; this.state.moderators = data.moderators;
this.state.admins = data.admins; this.state.admins = data.admins;
this.state.online = data.online; this.state.online = data.online;
document.title = `/c/${this.state.community.name} - ${WebSocketService.Instance.site.name}`; document.title = `/c/${this.state.community.name} - ${this.state.site.name}`;
this.setState(this.state); this.setState(this.state);
this.fetchData(); this.fetchData();
} else if (res.op == UserOperation.EditCommunity) { } else if (res.op == UserOperation.EditCommunity) {
@ -399,6 +421,10 @@ export class Community extends Component<any, State> {
let data = res.data as CommentResponse; let data = res.data as CommentResponse;
createCommentLikeRes(data, this.state.comments); createCommentLikeRes(data, this.state.comments);
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.site = data.site;
this.setState(this.state);
} }
} }
} }

View file

@ -1,19 +1,49 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { CommunityForm } from './community-form'; import { CommunityForm } from './community-form';
import { Community } from '../interfaces'; import {
import { WebSocketService } from '../services'; Community,
UserOperation,
WebSocketJsonResponse,
GetSiteResponse,
} from '../interfaces';
import { toast, wsJsonToRes } from '../utils';
import { WebSocketService, UserService } from '../services';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
export class CreateCommunity extends Component<any, any> { interface CreateCommunityState {
enableNsfw: boolean;
}
export class CreateCommunity extends Component<any, CreateCommunityState> {
private subscription: Subscription;
private emptyState: CreateCommunityState = {
enableNsfw: null,
};
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.handleCommunityCreate = this.handleCommunityCreate.bind(this); this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
this.state = this.emptyState;
if (!UserService.Instance.user) {
toast(i18n.t('not_logged_in'), 'danger');
this.context.router.history.push(`/login`);
}
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
WebSocketService.Instance.getSite();
} }
componentDidMount() { componentWillUnmount() {
document.title = `${i18n.t('create_community')} - ${ this.subscription.unsubscribe();
WebSocketService.Instance.site.name
}`;
} }
render() { render() {
@ -22,7 +52,10 @@ export class CreateCommunity extends Component<any, any> {
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> <div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t('create_community')}</h5> <h5>{i18n.t('create_community')}</h5>
<CommunityForm onCreate={this.handleCommunityCreate} /> <CommunityForm
onCreate={this.handleCommunityCreate}
enableNsfw={this.state.enableNsfw}
/>
</div> </div>
</div> </div>
</div> </div>
@ -32,4 +65,18 @@ export class CreateCommunity extends Component<any, any> {
handleCommunityCreate(community: Community) { handleCommunityCreate(community: Community) {
this.props.history.push(`/c/${community.name}`); this.props.history.push(`/c/${community.name}`);
} }
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
return;
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.enableNsfw = data.site.enable_nsfw;
this.setState(this.state);
document.title = `${i18n.t('create_community')} - ${data.site.name}`;
}
}
} }

View file

@ -1,19 +1,64 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { PostForm } from './post-form'; import { PostForm } from './post-form';
import { WebSocketService } from '../services'; import { toast, wsJsonToRes } from '../utils';
import { PostFormParams } from '../interfaces'; import { WebSocketService, UserService } from '../services';
import {
UserOperation,
PostFormParams,
WebSocketJsonResponse,
GetSiteResponse,
Site,
} from '../interfaces';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
export class CreatePost extends Component<any, any> { interface CreatePostState {
site: Site;
}
export class CreatePost extends Component<any, CreatePostState> {
private subscription: Subscription;
private emptyState: CreatePostState = {
site: {
id: undefined,
name: undefined,
creator_id: undefined,
published: undefined,
creator_name: undefined,
number_of_users: undefined,
number_of_posts: undefined,
number_of_comments: undefined,
number_of_communities: undefined,
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
},
};
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.handlePostCreate = this.handlePostCreate.bind(this); this.handlePostCreate = this.handlePostCreate.bind(this);
this.state = this.emptyState;
if (!UserService.Instance.user) {
toast(i18n.t('not_logged_in'), 'danger');
this.context.router.history.push(`/login`);
}
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
WebSocketService.Instance.getSite();
} }
componentDidMount() { componentWillUnmount() {
document.title = `${i18n.t('create_post')} - ${ this.subscription.unsubscribe();
WebSocketService.Instance.site.name
}`;
} }
render() { render() {
@ -22,7 +67,12 @@ export class CreatePost extends Component<any, any> {
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> <div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t('create_post')}</h5> <h5>{i18n.t('create_post')}</h5>
<PostForm onCreate={this.handlePostCreate} params={this.params} /> <PostForm
onCreate={this.handlePostCreate}
params={this.params}
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
/>
</div> </div>
</div> </div>
</div> </div>
@ -56,4 +106,18 @@ export class CreatePost extends Component<any, any> {
handlePostCreate(id: number) { handlePostCreate(id: number) {
this.props.history.push(`/post/${id}`); this.props.history.push(`/post/${id}`);
} }
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
return;
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.site = data.site;
this.setState(this.state);
document.title = `${i18n.t('create_post')} - ${data.site.name}`;
}
}
} }

View file

@ -1,22 +1,43 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { PrivateMessageForm } from './private-message-form'; import { PrivateMessageForm } from './private-message-form';
import { WebSocketService } from '../services'; import { WebSocketService, UserService } from '../services';
import { PrivateMessageFormParams } from '../interfaces'; import {
import { toast } from '../utils'; UserOperation,
WebSocketJsonResponse,
GetSiteResponse,
PrivateMessageFormParams,
} from '../interfaces';
import { toast, wsJsonToRes } from '../utils';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
export class CreatePrivateMessage extends Component<any, any> { export class CreatePrivateMessage extends Component<any, any> {
private subscription: Subscription;
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind( this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
this this
); );
if (!UserService.Instance.user) {
toast(i18n.t('not_logged_in'), 'danger');
this.context.router.history.push(`/login`);
}
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
WebSocketService.Instance.getSite();
} }
componentDidMount() { componentWillUnmount() {
document.title = `${i18n.t('create_private_message')} - ${ this.subscription.unsubscribe();
WebSocketService.Instance.site.name
}`;
} }
render() { render() {
@ -50,4 +71,18 @@ export class CreatePrivateMessage extends Component<any, any> {
// Navigate to the front // Navigate to the front
this.props.history.push(`/`); this.props.history.push(`/`);
} }
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
return;
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
document.title = `${i18n.t('create_private_message')} - ${
data.site.name
}`;
}
}
} }

View file

@ -16,6 +16,7 @@ import {
GetPrivateMessagesForm, GetPrivateMessagesForm,
PrivateMessagesResponse, PrivateMessagesResponse,
PrivateMessageResponse, PrivateMessageResponse,
GetSiteResponse,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { import {
@ -56,6 +57,7 @@ interface InboxState {
messages: Array<PrivateMessageI>; messages: Array<PrivateMessageI>;
sort: SortType; sort: SortType;
page: number; page: number;
enableDownvotes: boolean;
} }
export class Inbox extends Component<any, InboxState> { export class Inbox extends Component<any, InboxState> {
@ -68,6 +70,7 @@ export class Inbox extends Component<any, InboxState> {
messages: [], messages: [],
sort: SortType.New, sort: SortType.New,
page: 1, page: 1,
enableDownvotes: undefined,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -85,18 +88,13 @@ export class Inbox extends Component<any, InboxState> {
); );
this.refetch(); this.refetch();
WebSocketService.Instance.getSite();
} }
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
componentDidMount() {
document.title = `/u/${UserService.Instance.user.username} ${i18n.t(
'inbox'
)} - ${WebSocketService.Instance.site.name}`;
}
render() { render() {
return ( return (
<div class="container"> <div class="container">
@ -270,6 +268,7 @@ export class Inbox extends Component<any, InboxState> {
noIndent noIndent
markable markable
showContext showContext
enableDownvotes={this.state.enableDownvotes}
/> />
) : ( ) : (
<PrivateMessage privateMessage={i} /> <PrivateMessage privateMessage={i} />
@ -287,6 +286,7 @@ export class Inbox extends Component<any, InboxState> {
noIndent noIndent
markable markable
showContext showContext
enableDownvotes={this.state.enableDownvotes}
/> />
</div> </div>
); );
@ -301,6 +301,7 @@ export class Inbox extends Component<any, InboxState> {
noIndent noIndent
markable markable
showContext showContext
enableDownvotes={this.state.enableDownvotes}
/> />
))} ))}
</div> </div>
@ -328,12 +329,14 @@ export class Inbox extends Component<any, InboxState> {
{i18n.t('prev')} {i18n.t('prev')}
</button> </button>
)} )}
<button {this.unreadCount() > 0 && (
class="btn btn-sm btn-secondary" <button
onClick={linkEvent(this, this.nextPage)} class="btn btn-sm btn-secondary"
> onClick={linkEvent(this, this.nextPage)}
{i18n.t('next')} >
</button> {i18n.t('next')}
</button>
)}
</div> </div>
); );
} }
@ -522,19 +525,30 @@ export class Inbox extends Component<any, InboxState> {
let data = res.data as CommentResponse; let data = res.data as CommentResponse;
createCommentLikeRes(data, this.state.replies); createCommentLikeRes(data, this.state.replies);
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.enableDownvotes = data.site.enable_downvotes;
this.setState(this.state);
document.title = `/u/${UserService.Instance.user.username} ${i18n.t(
'inbox'
)} - ${data.site.name}`;
} }
} }
sendUnreadCount() { sendUnreadCount() {
let count = UserService.Instance.user.unreadCount = this.unreadCount();
this.state.replies.filter(r => !r.read).length +
this.state.mentions.filter(r => !r.read).length +
this.state.messages.filter(
r => !r.read && r.creator_id !== UserService.Instance.user.id
).length;
UserService.Instance.user.unreadCount = count;
UserService.Instance.sub.next({ UserService.Instance.sub.next({
user: UserService.Instance.user, user: UserService.Instance.user,
}); });
} }
unreadCount(): number {
return (
this.state.replies.filter(r => !r.read).length +
this.state.mentions.filter(r => !r.read).length +
this.state.messages.filter(
r => !r.read && r.creator_id !== UserService.Instance.user.id
).length
);
}
} }

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