forked from potsda.mn/mobilizon
Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
0a300ea5e5
|
@ -22,7 +22,7 @@
|
||||||
# In the latter case `**/*.{ex,exs}` will be used.
|
# In the latter case `**/*.{ex,exs}` will be used.
|
||||||
#
|
#
|
||||||
included: ["lib/", "src/", "test/"],
|
included: ["lib/", "src/", "test/"],
|
||||||
excluded: [~r"/_build/", ~r"/deps/", ~r"/js/"]
|
excluded: [~r"/_build/", ~r"/deps/", ~r"/src/"]
|
||||||
},
|
},
|
||||||
#
|
#
|
||||||
# If you create your own checks, you must specify the source files for
|
# If you create your own checks, you must specify the source files for
|
||||||
|
|
|
@ -1,44 +1,46 @@
|
||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/elixir-phoenix-postgres
|
// https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/elixir-phoenix-postgres
|
||||||
{
|
{
|
||||||
"name": "Elixir, Phoenix, Node.js & PostgresSQL (Community)",
|
"name": "Elixir, Phoenix, Node.js & PostgresSQL (Community)",
|
||||||
"dockerComposeFile": "docker-compose.yml",
|
"dockerComposeFile": "docker-compose.yml",
|
||||||
"service": "elixir",
|
"service": "elixir",
|
||||||
"workspaceFolder": "/workspace",
|
"workspaceFolder": "/workspace",
|
||||||
|
|
||||||
// Set *default* container specific settings.json values on container create.
|
// Set *default* container specific settings.json values on container create.
|
||||||
"settings": {
|
"settings": {
|
||||||
"sqltools.connections": [{
|
"sqltools.connections": [
|
||||||
"name": "Container database",
|
{
|
||||||
"driver": "PostgreSQL",
|
"name": "Container database",
|
||||||
"previewLimit": 50,
|
"driver": "PostgreSQL",
|
||||||
"server": "localhost",
|
"previewLimit": 50,
|
||||||
"port": 5432,
|
"server": "localhost",
|
||||||
"database": "postgres",
|
"port": 5432,
|
||||||
"username": "postgres",
|
"database": "postgres",
|
||||||
"password": "postgres"
|
"username": "postgres",
|
||||||
}]
|
"password": "postgres"
|
||||||
},
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
// Add the IDs of extensions you want installed when the container is created.
|
// Add the IDs of extensions you want installed when the container is created.
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"jakebecker.elixir-ls",
|
"jakebecker.elixir-ls",
|
||||||
"mtxr.sqltools",
|
"mtxr.sqltools",
|
||||||
"mtxr.sqltools-driver-pg"
|
"mtxr.sqltools-driver-pg"
|
||||||
],
|
],
|
||||||
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
"forwardPorts": [4000, 4001, 5432],
|
"forwardPorts": [4000, 4001, 5432],
|
||||||
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
// "postCreateCommand": "mix deps.get",
|
// "postCreateCommand": "mix deps.get",
|
||||||
// "runArgs": ["--userns=keep-id", "--privileged"],
|
// "runArgs": ["--userns=keep-id", "--privileged"],
|
||||||
// "containerUser": "vscode",
|
// "containerUser": "vscode",
|
||||||
// "containerEnv": {
|
// "containerEnv": {
|
||||||
// "HOME": "/home/vscode",
|
// "HOME": "/home/vscode",
|
||||||
// },
|
// },
|
||||||
// "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,Z",
|
// "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,Z",
|
||||||
|
|
||||||
// Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
|
// Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
|
||||||
"remoteUser": "vscode"
|
"remoteUser": "vscode"
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,5 +15,5 @@ Makefile
|
||||||
README.md
|
README.md
|
||||||
SECURITY.md
|
SECURITY.md
|
||||||
ssh_match_hostname
|
ssh_match_hostname
|
||||||
.js/package-lock.json
|
package-lock.json
|
||||||
js/node_modules
|
node_modules
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -46,10 +46,16 @@ release/
|
||||||
.weblate
|
.weblate
|
||||||
docker/production/.env
|
docker/production/.env
|
||||||
test-junit-report.xml
|
test-junit-report.xml
|
||||||
js/junit.xml
|
junit.xml
|
||||||
.env
|
.env
|
||||||
demo/
|
demo/
|
||||||
codeclimate.json
|
codeclimate.json
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
stats.html
|
||||||
|
/coverage
|
||||||
|
/playwright-report/
|
||||||
|
.histoire
|
||||||
|
|
||||||
# Nix out-links
|
# Nix out-links
|
||||||
result*
|
result*
|
||||||
|
|
|
@ -13,7 +13,6 @@ stages:
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
MIX_ENV: "test"
|
MIX_ENV: "test"
|
||||||
YARN_CACHE_FOLDER: "js/.yarn"
|
|
||||||
# DB Variables for Postgres / Postgis
|
# DB Variables for Postgres / Postgis
|
||||||
POSTGRES_DB: mobilizon_test
|
POSTGRES_DB: mobilizon_test
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
|
@ -38,8 +37,8 @@ cache:
|
||||||
paths:
|
paths:
|
||||||
- deps/
|
- deps/
|
||||||
- _build/
|
- _build/
|
||||||
- js/node_modules
|
- node_modules
|
||||||
- js/.yarn
|
- .npm
|
||||||
|
|
||||||
# Installed dependencies are cached across the pipeline
|
# Installed dependencies are cached across the pipeline
|
||||||
# So there is no need to reinstall them all the time
|
# So there is no need to reinstall them all the time
|
||||||
|
@ -47,7 +46,7 @@ cache:
|
||||||
install:
|
install:
|
||||||
stage: install
|
stage: install
|
||||||
script:
|
script:
|
||||||
- yarn --cwd "js" install --frozen-lockfile
|
- npm ci
|
||||||
- mix deps.get
|
- mix deps.get
|
||||||
- mix compile
|
- mix compile
|
||||||
|
|
||||||
|
@ -68,27 +67,26 @@ lint-elixir:
|
||||||
reports:
|
reports:
|
||||||
codequality: codeclimate.json
|
codequality: codeclimate.json
|
||||||
|
|
||||||
|
|
||||||
lint-front:
|
lint-front:
|
||||||
image: node:16
|
image: node:20
|
||||||
stage: check
|
stage: check
|
||||||
before_script:
|
before_script:
|
||||||
- export EXITVALUE=0
|
- export EXITVALUE=0
|
||||||
- yarn --cwd "js" install --frozen-lockfile
|
- npm ci
|
||||||
script:
|
script:
|
||||||
- yarn --cwd "js" run lint || export EXITVALUE=1
|
- npm run lint || export EXITVALUE=1
|
||||||
- yarn --cwd "js" run prettier -c . || export EXITVALUE=1
|
- npx prettier -c . || export EXITVALUE=1
|
||||||
- exit $EXITVALUE
|
- exit $EXITVALUE
|
||||||
|
|
||||||
build-frontend:
|
build-frontend:
|
||||||
stage: build-js
|
stage: build-js
|
||||||
image: node:16
|
image: node:20
|
||||||
before_script:
|
before_script:
|
||||||
- apt update
|
- apt update
|
||||||
- apt install -y --no-install-recommends python build-essential webp imagemagick gifsicle jpegoptim optipng pngquant
|
- apt install -y --no-install-recommends python3 build-essential webp imagemagick gifsicle jpegoptim optipng pngquant
|
||||||
script:
|
script:
|
||||||
- yarn --cwd "js" install --frozen-lockfile
|
- npm install --frozen-lockfile
|
||||||
- yarn --cwd "js" run build
|
- npm run build
|
||||||
artifacts:
|
artifacts:
|
||||||
expire_in: 5 days
|
expire_in: 5 days
|
||||||
paths:
|
paths:
|
||||||
|
@ -118,7 +116,7 @@ deps:
|
||||||
script:
|
script:
|
||||||
- export EXITVALUE=0
|
- export EXITVALUE=0
|
||||||
- mix hex.outdated || export EXITVALUE=1
|
- mix hex.outdated || export EXITVALUE=1
|
||||||
- yarn --cwd "js" outdated || export EXITVALUE=1
|
- npm outdated || export EXITVALUE=1
|
||||||
- exit $EXITVALUE
|
- exit $EXITVALUE
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
needs:
|
needs:
|
||||||
|
@ -151,16 +149,16 @@ vitest:
|
||||||
needs:
|
needs:
|
||||||
- lint-front
|
- lint-front
|
||||||
before_script:
|
before_script:
|
||||||
- yarn --cwd "js" install --frozen-lockfile
|
- npm install --frozen-lockfile
|
||||||
script:
|
script:
|
||||||
- yarn --cwd "js" run coverage --reporter=default --reporter=junit --outputFile.junit=./junit.xml
|
- npm run coverage --reporter=default --reporter=junit --outputFile.junit=./junit.xml
|
||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
paths:
|
paths:
|
||||||
- js/coverage
|
- coverage
|
||||||
reports:
|
reports:
|
||||||
junit:
|
junit:
|
||||||
- js/junit.xml
|
- junit.xml
|
||||||
expire_in: 30 days
|
expire_in: 30 days
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
|
@ -175,31 +173,31 @@ e2e:
|
||||||
- mix ecto.create
|
- mix ecto.create
|
||||||
- mix ecto.migrate
|
- mix ecto.migrate
|
||||||
- mix run priv/repo/e2e.seed.exs
|
- mix run priv/repo/e2e.seed.exs
|
||||||
- cd js && yarn install && yarn run build && npx playwright install && cd ../
|
- npm install && npm run build && npx playwright install
|
||||||
- mix phx.digest
|
- mix phx.digest
|
||||||
script:
|
script:
|
||||||
- mix phx.server &
|
- mix phx.server &
|
||||||
- cd js
|
|
||||||
- npx wait-on http://localhost:4000
|
- npx wait-on http://localhost:4000
|
||||||
- npx playwright test --project $BROWSER
|
- npx playwright test --project $BROWSER
|
||||||
parallel:
|
parallel:
|
||||||
matrix:
|
matrix:
|
||||||
- BROWSER: ['firefox', 'chromium']
|
- BROWSER: ["firefox", "chromium"]
|
||||||
artifacts:
|
artifacts:
|
||||||
expire_in: 2 days
|
expire_in: 2 days
|
||||||
paths:
|
paths:
|
||||||
- js/playwright-report/
|
- playwright-report/
|
||||||
- js/test-results/
|
- test-results/
|
||||||
|
|
||||||
pages:
|
pages:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
script:
|
script:
|
||||||
|
- mv public public-mbz
|
||||||
- mkdir public
|
- mkdir public
|
||||||
- mix deps.get
|
- mix deps.get
|
||||||
- mix docs
|
- mix docs
|
||||||
- mv doc public/backend
|
- mv doc public/backend
|
||||||
# #- yarn run --cwd "js" styleguide:build
|
# #- npm run styleguide:build
|
||||||
# #- mv js/styleguide public/frontend
|
# #- mv styleguide public/frontend
|
||||||
rules:
|
rules:
|
||||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||||
artifacts:
|
artifacts:
|
||||||
|
@ -270,7 +268,6 @@ build-and-push-to-latest-docker-tag:
|
||||||
- ARCH: ["arm64"]
|
- ARCH: ["arm64"]
|
||||||
ERL_FLAGS: ["ERL_FLAGS=+JMsingle true"]
|
ERL_FLAGS: ["ERL_FLAGS=+JMsingle true"]
|
||||||
|
|
||||||
|
|
||||||
# Don't push to latest when building beta/rc tags
|
# Don't push to latest when building beta/rc tags
|
||||||
build-and-push-docker-tag:
|
build-and-push-docker-tag:
|
||||||
<<: *docker
|
<<: *docker
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
cd js
|
npm run pre-commit
|
||||||
yarn run lint-staged
|
|
||||||
|
|
4
.prettierignore
Normal file
4
.prettierignore
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
src/i18n/*.json
|
||||||
|
coverage/
|
||||||
|
**/*.md
|
||||||
|
test/fixtures
|
|
@ -3,18 +3,18 @@
|
||||||
1C29EE70E90ECED01AF28EC58D2575B5
|
1C29EE70E90ECED01AF28EC58D2575B5
|
||||||
31CE26BC979C57B9E3CC97B40C290CE5
|
31CE26BC979C57B9E3CC97B40C290CE5
|
||||||
3529E7A4CECC24D02678820E6F521162
|
3529E7A4CECC24D02678820E6F521162
|
||||||
4A4B7002DEB734A943B467DF7D2BD1AA
|
37E854EA3BDF7275C6A7631F80804EC4
|
||||||
4E7C044C59E0BCB76AA826789998F624
|
4E7C044C59E0BCB76AA826789998F624
|
||||||
53CBBEB6243FAF5C37249CBA17DE6F4C
|
53CBBEB6243FAF5C37249CBA17DE6F4C
|
||||||
5BCE3651A03711295046DE48BDFE007E
|
5BCE3651A03711295046DE48BDFE007E
|
||||||
|
5C16A2AE6A24E4795F95DDE20EEC458E
|
||||||
|
5C4CED447689F00D9D1ACEB9B895ED29
|
||||||
630C0972985257251EDF89A7117DE423
|
630C0972985257251EDF89A7117DE423
|
||||||
8274AF29B81EC7CF7F585D5EED8A64E1
|
|
||||||
94ACF7B17C3FF42F64E57DD1DA936BD8
|
94ACF7B17C3FF42F64E57DD1DA936BD8
|
||||||
A32E125003F1EDFAD95C487C6A969725
|
A32E125003F1EDFAD95C487C6A969725
|
||||||
ACF6272A1DBB3A2ABD96C0C120B5CA69
|
ACF6272A1DBB3A2ABD96C0C120B5CA69
|
||||||
C46C4893B2F702ACADC4CAA5683FE370
|
C46C4893B2F702ACADC4CAA5683FE370
|
||||||
CDF2CCE0CF10F49CDFAE22FE26208155
|
CDF2CCE0CF10F49CDFAE22FE26208155
|
||||||
E720CB13C50FF3ADEE7C522531E11217
|
E720CB13C50FF3ADEE7C522531E11217
|
||||||
E8FC5F2C5DEA6671BA596B022C4FE6F2
|
|
||||||
F3D5851D3FB050939841ED2F14307A27
|
F3D5851D3FB050939841ED2F14307A27
|
||||||
FD1C9756370A195B74E95CE504C45E9E
|
FD1C9756370A195B74E95CE504C45E9E
|
|
@ -1,6 +1,6 @@
|
||||||
FROM elixir:1.13.4-alpine
|
FROM elixir:1.13.4-alpine
|
||||||
|
|
||||||
RUN apk add --no-cache inotify-tools postgresql-client yarn file make gcc libc-dev argon2 imagemagick cmake build-base libwebp-tools bash ncurses git python3
|
RUN apk add --no-cache inotify-tools postgresql-client file make gcc libc-dev argon2 imagemagick cmake build-base libwebp-tools bash ncurses git python3
|
||||||
|
|
||||||
RUN mix local.hex --force && mix local.rebar --force
|
RUN mix local.hex --force && mix local.rebar --force
|
||||||
|
|
||||||
|
|
3
Makefile
3
Makefile
|
@ -4,7 +4,7 @@ init:
|
||||||
|
|
||||||
setup: stop
|
setup: stop
|
||||||
@bash docker/message.sh "Compiling everything"
|
@bash docker/message.sh "Compiling everything"
|
||||||
docker-compose run --rm api bash -c 'mix deps.get; yarn --cwd "js"; yarn --cwd "js" build:pictures; mix ecto.create; mix ecto.migrate'
|
docker-compose run --rm api bash -c 'mix deps.get; npm ci; npm run build:pictures; mix ecto.create; mix ecto.migrate'
|
||||||
migrate:
|
migrate:
|
||||||
docker-compose run --rm api mix ecto.migrate
|
docker-compose run --rm api mix ecto.migrate
|
||||||
logs:
|
logs:
|
||||||
|
@ -19,6 +19,7 @@ stop:
|
||||||
@bash docker/message.sh "Mobilizon is stopped"
|
@bash docker/message.sh "Mobilizon is stopped"
|
||||||
test: stop
|
test: stop
|
||||||
@bash docker/message.sh "Running tests"
|
@bash docker/message.sh "Running tests"
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.test.yml run api mix prepare_test
|
||||||
docker-compose -f docker-compose.yml -f docker-compose.test.yml run api mix test $(only)
|
docker-compose -f docker-compose.yml -f docker-compose.test.yml run api mix test $(only)
|
||||||
@bash docker/message.sh "Done running tests"
|
@bash docker/message.sh "Done running tests"
|
||||||
format:
|
format:
|
||||||
|
|
|
@ -7,6 +7,6 @@ module.exports = {
|
||||||
localSchemaFile: "./schema.graphql",
|
localSchemaFile: "./schema.graphql",
|
||||||
},
|
},
|
||||||
// Files processed by the extension
|
// Files processed by the extension
|
||||||
includes: ["js/src/**/*.vue", "js/src/**/*.js"],
|
includes: ["src/**/*.vue", "src/**/*.js"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,8 +15,7 @@ config :mobilizon, Mobilizon.Web.Endpoint,
|
||||||
check_origin: false,
|
check_origin: false,
|
||||||
watchers: [
|
watchers: [
|
||||||
node: [
|
node: [
|
||||||
"node_modules/.bin/vite",
|
"node_modules/.bin/vite"
|
||||||
cd: Path.expand("../js", __DIR__)
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ config :mobilizon, Mobilizon.Web.Endpoint,
|
||||||
check_origin: false,
|
check_origin: false,
|
||||||
# Somehow this can't be merged properly with the dev config so we got this…
|
# Somehow this can't be merged properly with the dev config so we got this…
|
||||||
watchers: [
|
watchers: [
|
||||||
yarn: [cd: Path.expand("../js", __DIR__)]
|
npm: []
|
||||||
]
|
]
|
||||||
|
|
||||||
config :vite_phx,
|
config :vite_phx,
|
||||||
|
|
|
@ -110,7 +110,7 @@ mixRelease rec {
|
||||||
# Install the compiled js part
|
# Install the compiled js part
|
||||||
preBuild =
|
preBuild =
|
||||||
''
|
''
|
||||||
cp -a "${mobilizon-js}/libexec/mobilizon/deps/priv/static" ./priv
|
cp -a "${mobilizon-js}/_napalm-install/priv/static" ./priv
|
||||||
chmod 770 -R ./priv
|
chmod 770 -R ./priv
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ services:
|
||||||
MIX_ENV: "test"
|
MIX_ENV: "test"
|
||||||
MOBILIZON_DATABASE_DBNAME: mobilizon_test
|
MOBILIZON_DATABASE_DBNAME: mobilizon_test
|
||||||
MOBILIZON_INSTANCE_HOST: mobilizon.test
|
MOBILIZON_INSTANCE_HOST: mobilizon.test
|
||||||
command: "mix test"
|
command: "mix prepare_test && mix test"
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
.:
|
.:
|
||||||
|
|
|
@ -18,9 +18,8 @@ ENV NODE_VERSION 18
|
||||||
RUN apt-get update -yq && apt-get install -yq build-essential cmake postgresql-client git curl gnupg unzip exiftool webp imagemagick gifsicle
|
RUN apt-get update -yq && apt-get install -yq build-essential cmake postgresql-client git curl gnupg unzip exiftool webp imagemagick gifsicle
|
||||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
# # Install Node & yarn
|
# # Install Node
|
||||||
# RUN curl -sL https://deb.nodesource.com/setup_16.x | bash && apt-get install nodejs -yq
|
# RUN curl -sL https://deb.nodesource.com/setup_16.x | bash && apt-get install nodejs -yq
|
||||||
# RUN npm install -g yarn
|
|
||||||
|
|
||||||
# Install build tools
|
# Install build tools
|
||||||
RUN source /root/.bashrc && \
|
RUN source /root/.bashrc && \
|
||||||
|
@ -32,8 +31,8 @@ COPY ./ /mobilizon
|
||||||
WORKDIR /mobilizon
|
WORKDIR /mobilizon
|
||||||
|
|
||||||
# # Build front-end
|
# # Build front-end
|
||||||
# RUN yarn --cwd "js" install --frozen-lockfile
|
# RUN npm install
|
||||||
# RUN yarn --cwd "js" run build
|
# RUN npm run build
|
||||||
|
|
||||||
# Elixir release
|
# Elixir release
|
||||||
RUN source /root/.bashrc && \
|
RUN source /root/.bashrc && \
|
||||||
|
|
|
@ -3,11 +3,10 @@ FROM node:18-alpine as assets
|
||||||
|
|
||||||
RUN apk add --no-cache python3 build-base libwebp-tools bash imagemagick ncurses
|
RUN apk add --no-cache python3 build-base libwebp-tools bash imagemagick ncurses
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY js .
|
COPY . .
|
||||||
|
|
||||||
# Network timeout because it's slow when cross-compiling
|
# Network timeout because it's slow when cross-compiling
|
||||||
RUN yarn install --network-timeout 100000 \
|
RUN npm install && npm run build
|
||||||
&& yarn run build
|
|
||||||
|
|
||||||
# Then, build the application binary
|
# Then, build the application binary
|
||||||
FROM elixir:1.15-alpine AS builder
|
FROM elixir:1.15-alpine AS builder
|
||||||
|
|
|
@ -4,7 +4,7 @@ LABEL maintainer="Thomas Citharel <tcit@tcit.fr>"
|
||||||
ENV REFRESHED_AT=2023-08-17
|
ENV REFRESHED_AT=2023-08-17
|
||||||
RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg xvfb libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 cmake exiftool python3-pip python3-setuptools
|
RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg xvfb libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 cmake exiftool python3-pip python3-setuptools
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash && apt-get install nodejs -yq
|
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash && apt-get install nodejs -yq
|
||||||
RUN npm install -g yarn wait-on
|
RUN npm install -g wait-on
|
||||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
RUN mix local.hex --force && mix local.rebar --force
|
RUN mix local.hex --force && mix local.rebar --force
|
||||||
RUN pip3 install -Iv weasyprint pyexcel_ods3
|
RUN pip3 install -Iv weasyprint pyexcel_ods3
|
||||||
|
|
|
@ -19,7 +19,7 @@ Mobilizon is an app that uses:
|
||||||
|
|
||||||
* `config` backend compile-time and runtime configuration
|
* `config` backend compile-time and runtime configuration
|
||||||
* `docker` 🐳
|
* `docker` 🐳
|
||||||
* `js/src` Front-end
|
* `src` Front-end
|
||||||
* `lib/federation` Handling all the federation stuff (sending and receving activities, converting activities, signatures, helpers…)
|
* `lib/federation` Handling all the federation stuff (sending and receving activities, converting activities, signatures, helpers…)
|
||||||
* `lib/graphql/schema` The schema declarations for the GraphQL API
|
* `lib/graphql/schema` The schema declarations for the GraphQL API
|
||||||
* `lib/graphql/resolvers` The logic behind the GraphQL API
|
* `lib/graphql/resolvers` The logic behind the GraphQL API
|
||||||
|
|
0
js/env.d.ts → env.d.ts
vendored
0
js/env.d.ts → env.d.ts
vendored
37
flake.lock
37
flake.lock
|
@ -1,5 +1,41 @@
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1659877975,
|
||||||
|
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"napalm": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1693989153,
|
||||||
|
"narHash": "sha256-gx39Y3opGB25+44OjM+h1bdJyzgLD963va8ULGYlbhM=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "napalm",
|
||||||
|
"rev": "a8215ccf1c80070f51a92771f3bc637dd9b9f7ee",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "napalm",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nix-filter": {
|
"nix-filter": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1694857738,
|
"lastModified": 1694857738,
|
||||||
|
@ -33,6 +69,7 @@
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"napalm": "napalm",
|
||||||
"nix-filter": "nix-filter",
|
"nix-filter": "nix-filter",
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
|
|
36
flake.nix
36
flake.nix
|
@ -4,9 +4,11 @@
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
nix-filter.url = "github:numtide/nix-filter";
|
nix-filter.url = "github:numtide/nix-filter";
|
||||||
|
napalm.url = "github:nix-community/napalm";
|
||||||
|
napalm.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, nix-filter }:
|
outputs = { self, nixpkgs, nix-filter, napalm }:
|
||||||
let
|
let
|
||||||
forAllSystems = f: nixpkgs.lib.genAttrs
|
forAllSystems = f: nixpkgs.lib.genAttrs
|
||||||
[ "x86_64-linux" "aarch64-linux" ]
|
[ "x86_64-linux" "aarch64-linux" ]
|
||||||
|
@ -28,15 +30,37 @@
|
||||||
src = filter {
|
src = filter {
|
||||||
root = ./.;
|
root = ./.;
|
||||||
exclude = [
|
exclude = [
|
||||||
|
"flake.lock"
|
||||||
(filter.matchExt "nix")
|
(filter.matchExt "nix")
|
||||||
"js"
|
"src"
|
||||||
|
(filter.matchExt "js")
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
src-config = ./config;
|
src-config = ./config;
|
||||||
mobilizon-js = self.packages.x86_64-linux.mobilizon-frontend;
|
mobilizon-js = self.packages.x86_64-linux.mobilizon-frontend;
|
||||||
};
|
};
|
||||||
|
|
||||||
mobilizon-frontend = pkgs.callPackage ./js { inherit filter; };
|
mobilizon-frontend =
|
||||||
|
let
|
||||||
|
nodejs = pkgs.nodejs-18_x;
|
||||||
|
nodeEnv = pkgs.callPackage ./node-env.nix { inherit nodejs; };
|
||||||
|
nodePackages = pkgs.callPackage ./node-packages.nix { inherit nodeEnv; };
|
||||||
|
in
|
||||||
|
napalm.legacyPackages."${system}".buildPackage
|
||||||
|
(filter {
|
||||||
|
root = ./.;
|
||||||
|
exclude = [
|
||||||
|
"flake.lock"
|
||||||
|
(filter.matchExt "nix")
|
||||||
|
"lib"
|
||||||
|
"config"
|
||||||
|
];
|
||||||
|
})
|
||||||
|
{
|
||||||
|
inherit nodejs;
|
||||||
|
nativeBuildInputs = [ pkgs.imagemagick ];
|
||||||
|
npmCommands = [ "npm install" "npm run build" ];
|
||||||
|
};
|
||||||
|
|
||||||
default = self.packages.x86_64-linux.mobilizon;
|
default = self.packages.x86_64-linux.mobilizon;
|
||||||
|
|
||||||
|
@ -59,11 +83,9 @@
|
||||||
mix2nix
|
mix2nix
|
||||||
cmake
|
cmake
|
||||||
imagemagick
|
imagemagick
|
||||||
(yarn.override {
|
|
||||||
nodejs = nodejs-18_x;
|
|
||||||
})
|
|
||||||
yarn2nix
|
|
||||||
nodejs-18_x
|
nodejs-18_x
|
||||||
|
node2nix
|
||||||
|
prefetch-npm-deps
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,7 @@ export default defineConfig({
|
||||||
plugins: [HstVue()],
|
plugins: [HstVue()],
|
||||||
setupFile: path.resolve(__dirname, "./src/histoire.setup.ts"),
|
setupFile: path.resolve(__dirname, "./src/histoire.setup.ts"),
|
||||||
viteNodeInlineDeps: [/date-fns/],
|
viteNodeInlineDeps: [/date-fns/],
|
||||||
|
// viteIgnorePlugins: ['vite-plugin-pwa', 'vite-plugin-pwa:build', 'vite-plugin-pwa:info'],
|
||||||
tree: {
|
tree: {
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
27
js/.gitignore
vendored
27
js/.gitignore
vendored
|
@ -1,27 +0,0 @@
|
||||||
.DS_Store
|
|
||||||
node_modules
|
|
||||||
/dist
|
|
||||||
|
|
||||||
/coverage
|
|
||||||
stats.html
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
|
|
||||||
# Log files
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.idea
|
|
||||||
.vscode
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
/test-results/
|
|
||||||
/playwright-report/
|
|
||||||
/playwright/.cache/
|
|
|
@ -1,2 +0,0 @@
|
||||||
src/i18n/*.json
|
|
||||||
coverage/
|
|
|
@ -1,35 +0,0 @@
|
||||||
{ lib, filter, yarn, mkYarnPackage, imagemagick }:
|
|
||||||
|
|
||||||
mkYarnPackage rec {
|
|
||||||
src = filter {
|
|
||||||
root = ./.;
|
|
||||||
exclude = [ (filter.matchExt "nix") ];
|
|
||||||
};
|
|
||||||
|
|
||||||
# Somehow $out/deps/mobilizon/node_modules ends up only containing a .bin
|
|
||||||
# directory otherwise.
|
|
||||||
yarnPostBuild = ''
|
|
||||||
rm -rf $out/deps/mobilizon/node_modules
|
|
||||||
ln -s $out/node_modules $out/deps/mobilizon/node_modules
|
|
||||||
'';
|
|
||||||
|
|
||||||
buildPhase = ''
|
|
||||||
runHook preBuild
|
|
||||||
|
|
||||||
yarn run build
|
|
||||||
|
|
||||||
runHook postBuild
|
|
||||||
'';
|
|
||||||
|
|
||||||
doCheck = true;
|
|
||||||
checkPhase = "yarn run test";
|
|
||||||
|
|
||||||
nativeBuildInputs = [ imagemagick ];
|
|
||||||
|
|
||||||
meta = with lib; {
|
|
||||||
description = "Frontend for the Mobilizon server";
|
|
||||||
homepage = "https://joinmobilizon.org/";
|
|
||||||
license = licenses.agpl3Plus;
|
|
||||||
maintainers = with maintainers; [ minijackson erictapen ];
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { FILTER_TAGS } from "@/graphql/tags";
|
|
||||||
import { ITag } from "@/types/tag.model";
|
|
||||||
import { apolloClient, waitApolloQuery } from "@/vue-apollo";
|
|
||||||
import { provideApolloClient, useQuery } from "@vue/apollo-composable";
|
|
||||||
|
|
||||||
export async function fetchTags(text: string): Promise<ITag[]> {
|
|
||||||
try {
|
|
||||||
const res = await waitApolloQuery(
|
|
||||||
provideApolloClient(apolloClient)(() =>
|
|
||||||
useQuery<{ tags: ITag[] }, { filter: string }>(FILTER_TAGS, {
|
|
||||||
filter: text,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return res.data.tags;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
5
js/src/typings/absinthe.d.ts
vendored
5
js/src/typings/absinthe.d.ts
vendored
|
@ -1,5 +0,0 @@
|
||||||
declare module "@absinthe/socket";
|
|
||||||
|
|
||||||
declare module "@absinthe/socket-apollo-link";
|
|
||||||
|
|
||||||
declare module "apollo-absinthe-upload-link";
|
|
|
@ -1,38 +0,0 @@
|
||||||
import {
|
|
||||||
ApolloClient,
|
|
||||||
ApolloQueryResult,
|
|
||||||
NormalizedCacheObject,
|
|
||||||
OperationVariables,
|
|
||||||
} from "@apollo/client/core";
|
|
||||||
import buildCurrentUserResolver from "@/apollo/user";
|
|
||||||
import { cache } from "./apollo/memory";
|
|
||||||
import { fullLink } from "./apollo/link";
|
|
||||||
import { UseQueryReturn } from "@vue/apollo-composable";
|
|
||||||
|
|
||||||
export const apolloClient = new ApolloClient<NormalizedCacheObject>({
|
|
||||||
cache,
|
|
||||||
link: fullLink,
|
|
||||||
connectToDevTools: true,
|
|
||||||
resolvers: buildCurrentUserResolver(cache),
|
|
||||||
});
|
|
||||||
|
|
||||||
export function waitApolloQuery<
|
|
||||||
TResult = any,
|
|
||||||
TVariables extends OperationVariables = OperationVariables,
|
|
||||||
>({
|
|
||||||
onResult,
|
|
||||||
onError,
|
|
||||||
}: UseQueryReturn<TResult, TVariables>): Promise<ApolloQueryResult<TResult>> {
|
|
||||||
return new Promise((res, rej) => {
|
|
||||||
const { off: offResult } = onResult((result) => {
|
|
||||||
if (result.loading === false) {
|
|
||||||
offResult();
|
|
||||||
res(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const { off: offError } = onError((error) => {
|
|
||||||
offError();
|
|
||||||
rej(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
// Vitest Snapshot v1
|
|
||||||
|
|
||||||
exports[`CommentTree > renders a comment tree with comments 1`] = `
|
|
||||||
"<div data-v-5d0380ab=\\"\\">
|
|
||||||
<form class=\\"\\" data-v-5d0380ab=\\"\\">
|
|
||||||
<!--v-if-->
|
|
||||||
<article class=\\"flex flex-wrap items-start gap-2\\" data-v-5d0380ab=\\"\\">
|
|
||||||
<figure class=\\"\\" data-v-5d0380ab=\\"\\">
|
|
||||||
<identity-picker-wrapper-stub modelvalue=\\"[object Object]\\" inline=\\"false\\" masked=\\"false\\" data-v-5d0380ab=\\"\\"></identity-picker-wrapper-stub>
|
|
||||||
</figure>
|
|
||||||
<div class=\\"flex-1\\" data-v-5d0380ab=\\"\\">
|
|
||||||
<div class=\\"flex flex-col gap-2\\" data-v-5d0380ab=\\"\\">
|
|
||||||
<div class=\\"editor-wrapper\\" data-v-5d0380ab=\\"\\">
|
|
||||||
<editor-stub currentactor=\\"[object Object]\\" mode=\\"comment\\" modelvalue=\\"\\" aria-label=\\"Comment body\\" data-v-5d0380ab=\\"\\"></editor-stub>
|
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=\\"\\" data-v-5d0380ab=\\"\\">
|
|
||||||
<o-button-stub variant=\\"primary\\" iconleft=\\"send\\" rounded=\\"false\\" outlined=\\"false\\" expanded=\\"false\\" inverted=\\"false\\" nativetype=\\"submit\\" tag=\\"button\\" disabled=\\"false\\" iconboth=\\"false\\" data-v-5d0380ab=\\"\\"></o-button-stub>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</form>
|
|
||||||
<transition-group-stub data-v-5d0380ab=\\"\\">
|
|
||||||
<transition-group-stub data-v-5d0380ab=\\"\\">
|
|
||||||
<comment-stub comment=\\"[object Object]\\" event=\\"[object Object]\\" currentactor=\\"[object Object]\\" rootcomment=\\"true\\" class=\\"root-comment\\" data-v-5d0380ab=\\"\\"></comment-stub>
|
|
||||||
<comment-stub comment=\\"[object Object]\\" event=\\"[object Object]\\" currentactor=\\"[object Object]\\" rootcomment=\\"true\\" class=\\"root-comment\\" data-v-5d0380ab=\\"\\"></comment-stub>
|
|
||||||
</transition-group-stub>
|
|
||||||
</transition-group-stub>
|
|
||||||
</div>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`CommentTree > renders a loading comment tree 1`] = `
|
|
||||||
"<div data-v-5d0380ab=\\"\\">
|
|
||||||
<!--v-if-->
|
|
||||||
<p class=\\"text-center\\" data-v-5d0380ab=\\"\\">Loading comments…</p>
|
|
||||||
</div>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`CommentTree > renders an empty comment tree 1`] = `
|
|
||||||
"<div data-v-5d0380ab=\\"\\">
|
|
||||||
<form class=\\"\\" data-v-5d0380ab=\\"\\">
|
|
||||||
<!--v-if-->
|
|
||||||
<article class=\\"flex flex-wrap items-start gap-2\\" data-v-5d0380ab=\\"\\">
|
|
||||||
<figure class=\\"\\" data-v-5d0380ab=\\"\\">
|
|
||||||
<identity-picker-wrapper-stub modelvalue=\\"[object Object]\\" inline=\\"false\\" masked=\\"false\\" data-v-5d0380ab=\\"\\"></identity-picker-wrapper-stub>
|
|
||||||
</figure>
|
|
||||||
<div class=\\"flex-1\\" data-v-5d0380ab=\\"\\">
|
|
||||||
<div class=\\"flex flex-col gap-2\\" data-v-5d0380ab=\\"\\">
|
|
||||||
<div class=\\"editor-wrapper\\" data-v-5d0380ab=\\"\\">
|
|
||||||
<editor-stub currentactor=\\"[object Object]\\" mode=\\"comment\\" modelvalue=\\"\\" aria-label=\\"Comment body\\" data-v-5d0380ab=\\"\\"></editor-stub>
|
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=\\"\\" data-v-5d0380ab=\\"\\">
|
|
||||||
<o-button-stub variant=\\"primary\\" iconleft=\\"send\\" rounded=\\"false\\" outlined=\\"false\\" expanded=\\"false\\" inverted=\\"false\\" nativetype=\\"submit\\" tag=\\"button\\" disabled=\\"false\\" iconboth=\\"false\\" data-v-5d0380ab=\\"\\"></o-button-stub>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</form>
|
|
||||||
<transition-group-stub data-v-5d0380ab=\\"\\">
|
|
||||||
<empty-content-stub icon=\\"comment\\" descriptionclasses=\\"\\" inline=\\"true\\" center=\\"false\\" data-v-5d0380ab=\\"\\"></empty-content-stub>
|
|
||||||
</transition-group-stub>
|
|
||||||
</div>"
|
|
||||||
`;
|
|
|
@ -1,37 +0,0 @@
|
||||||
// Vitest Snapshot v1
|
|
||||||
|
|
||||||
exports[`PostListItem > renders post list item with basic informations 1`] = `
|
|
||||||
"<a href=\\"/p/my-blog-post-some-uuid\\" class=\\"block md:flex bg-white dark:bg-violet-2 dark:text-white dark:hover:text-white rounded-lg shadow-md\\" dir=\\"auto\\" data-v-6ca7cc69=\\"\\">
|
|
||||||
<!--v-if-->
|
|
||||||
<div class=\\"flex flex-col gap-1 bg-inherit p-2 rounded-lg flex-1\\" data-v-6ca7cc69=\\"\\">
|
|
||||||
<h3 class=\\"text-xl color-violet-3 line-clamp-3 mb-2 font-bold\\" lang=\\"en\\" data-v-6ca7cc69=\\"\\">My Blog Post</h3>
|
|
||||||
<p class=\\"flex gap-2\\" data-v-6ca7cc69=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon clock-icon\\" role=\\"img\\" data-v-6ca7cc69=\\"\\"><svg fill=\\"currentColor\\" class=\\"material-design-icon__svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\"><path d=\\"M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.9L16.2,16.2Z\\"><!--v-if--></path></svg></span><span dir=\\"auto\\" class=\\"\\" data-v-6ca7cc69=\\"\\">Dec 2, 2020</span></p>
|
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
|
||||||
</a>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`PostListItem > renders post list item with publisher name 1`] = `
|
|
||||||
"<a href=\\"/p/my-blog-post-some-uuid\\" class=\\"block md:flex bg-white dark:bg-violet-2 dark:text-white dark:hover:text-white rounded-lg shadow-md\\" dir=\\"auto\\" data-v-6ca7cc69=\\"\\">
|
|
||||||
<!--v-if-->
|
|
||||||
<div class=\\"flex flex-col gap-1 bg-inherit p-2 rounded-lg flex-1\\" data-v-6ca7cc69=\\"\\">
|
|
||||||
<h3 class=\\"text-xl color-violet-3 line-clamp-3 mb-2 font-bold\\" lang=\\"en\\" data-v-6ca7cc69=\\"\\">My Blog Post</h3>
|
|
||||||
<p class=\\"flex gap-2\\" data-v-6ca7cc69=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon clock-icon\\" role=\\"img\\" data-v-6ca7cc69=\\"\\"><svg fill=\\"currentColor\\" class=\\"material-design-icon__svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\"><path d=\\"M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.9L16.2,16.2Z\\"><!--v-if--></path></svg></span><span dir=\\"auto\\" class=\\"\\" data-v-6ca7cc69=\\"\\">Dec 2, 2020</span></p>
|
|
||||||
<!--v-if-->
|
|
||||||
<p class=\\"flex gap-1\\" data-v-6ca7cc69=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon account-edit-icon\\" role=\\"img\\" data-v-6ca7cc69=\\"\\"><svg fill=\\"currentColor\\" class=\\"material-design-icon__svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\"><path d=\\"M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z\\"><!--v-if--></path></svg></span>Published by <b class=\\"\\" data-v-6ca7cc69=\\"\\">An author</b></p>
|
|
||||||
</div>
|
|
||||||
</a>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`PostListItem > renders post list item with tags 1`] = `
|
|
||||||
"<a href=\\"/p/my-blog-post-some-uuid\\" class=\\"block md:flex bg-white dark:bg-violet-2 dark:text-white dark:hover:text-white rounded-lg shadow-md\\" dir=\\"auto\\" data-v-6ca7cc69=\\"\\">
|
|
||||||
<!--v-if-->
|
|
||||||
<div class=\\"flex flex-col gap-1 bg-inherit p-2 rounded-lg flex-1\\" data-v-6ca7cc69=\\"\\">
|
|
||||||
<h3 class=\\"text-xl color-violet-3 line-clamp-3 mb-2 font-bold\\" lang=\\"en\\" data-v-6ca7cc69=\\"\\">My Blog Post</h3>
|
|
||||||
<p class=\\"flex gap-2\\" data-v-6ca7cc69=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon clock-icon\\" role=\\"img\\" data-v-6ca7cc69=\\"\\"><svg fill=\\"currentColor\\" class=\\"material-design-icon__svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\"><path d=\\"M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.9L16.2,16.2Z\\"><!--v-if--></path></svg></span><span dir=\\"auto\\" class=\\"\\" data-v-6ca7cc69=\\"\\">Dec 2, 2020</span></p>
|
|
||||||
<div class=\\"flex flex-wrap gap-y-0 gap-x-2\\" data-v-6ca7cc69=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon tag-icon\\" role=\\"img\\" data-v-6ca7cc69=\\"\\"><svg fill=\\"currentColor\\" class=\\"material-design-icon__svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\"><path d=\\"M5.5,7A1.5,1.5 0 0,1 4,5.5A1.5,1.5 0 0,1 5.5,4A1.5,1.5 0 0,1 7,5.5A1.5,1.5 0 0,1 5.5,7M21.41,11.58L12.41,2.58C12.05,2.22 11.55,2 11,2H4C2.89,2 2,2.89 2,4V11C2,11.55 2.22,12.05 2.59,12.41L11.58,21.41C11.95,21.77 12.45,22 13,22C13.55,22 14.05,21.77 14.41,21.41L21.41,14.41C21.78,14.05 22,13.55 22,13C22,12.44 21.77,11.94 21.41,11.58Z\\"><!--v-if--></path></svg></span><span class=\\"rounded-md truncate text-sm text-violet-title px-2 py-1 bg-purple-3 dark:text-violet-3\\" data-v-6955ca87=\\"\\" data-v-6ca7cc69=\\"\\">A tag</span></div>
|
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
|
||||||
</a>"
|
|
||||||
`;
|
|
|
@ -1,29 +0,0 @@
|
||||||
// Vitest Snapshot v1
|
|
||||||
|
|
||||||
exports[`ReportModal > renders report modal with basic informations and submits it 1`] = `
|
|
||||||
"<div class=\\"p-2\\" data-v-e0cceef3=\\"\\">
|
|
||||||
<!--v-if-->
|
|
||||||
<section data-v-e0cceef3=\\"\\">
|
|
||||||
<div class=\\"flex gap-1 flex-row mb-3\\" data-v-e0cceef3=\\"\\"><span class=\\"o-icon o-icon--warning hidden md:block flex-1\\" data-v-e0cceef3=\\"\\"><i class=\\"mdi mdi-alert 48\\"></i></span>
|
|
||||||
<p data-v-e0cceef3=\\"\\">The report will be sent to the moderators of your instance. You can explain why you report this content below.</p>
|
|
||||||
</div>
|
|
||||||
<div class=\\"\\" data-v-e0cceef3=\\"\\">
|
|
||||||
<!--v-if-->
|
|
||||||
<div class=\\"o-field o-field--filled\\" data-v-e0cceef3=\\"\\"><label for=\\"additional-comments\\" class=\\"o-field__label\\">Additional comments</label>
|
|
||||||
<div class=\\"o-ctrl-input\\" data-v-e0cceef3=\\"\\"><textarea id=\\"additional-comments\\" class=\\"o-input o-input__textarea\\"></textarea>
|
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<footer class=\\"flex gap-2 py-3\\" data-v-e0cceef3=\\"\\"><button type=\\"button\\" class=\\"o-btn o-btn--outlined\\" data-v-e0cceef3=\\"\\"><span class=\\"o-btn__wrapper\\"><!--v-if--><span class=\\"o-btn__label\\">Cancel</span>
|
|
||||||
<!--v-if--></span>
|
|
||||||
</button><button type=\\"button\\" class=\\"o-btn o-btn--primary\\" data-v-e0cceef3=\\"\\"><span class=\\"o-btn__wrapper\\"><!--v-if--><span class=\\"o-btn__label\\">Send the report</span>
|
|
||||||
<!--v-if--></span>
|
|
||||||
</button></footer>
|
|
||||||
</div>"
|
|
||||||
`;
|
|
|
@ -1,196 +0,0 @@
|
||||||
// Vitest Snapshot v1
|
|
||||||
|
|
||||||
exports[`App component > renders a Vue component 1`] = `
|
|
||||||
"<nav class=\\"bg-white border-gray-200 px-2 sm:px-4 py-2.5 dark:bg-zinc-900\\" id=\\"navbar\\">
|
|
||||||
<div class=\\"container mx-auto flex flex-wrap items-center mx-auto gap-4\\">
|
|
||||||
<router-link to=\\"[object Object]\\" class=\\"flex items-center\\">
|
|
||||||
<mobilizon-logo-stub invert=\\"false\\" class=\\"w-40\\"></mobilizon-logo-stub>
|
|
||||||
</router-link>
|
|
||||||
<!--v-if--><button type=\\"button\\" class=\\"inline-flex items-center p-2 ml-1 text-sm text-zinc-500 rounded-lg md:hidden hover:bg-zinc-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-zinc-400 dark:hover:bg-zinc-700 dark:focus:ring-gray-600\\" aria-controls=\\"mobile-menu-2\\" aria-expanded=\\"false\\"><span class=\\"sr-only\\">Open main menu</span><svg class=\\"w-6 h-6\\" aria-hidden=\\"true\\" fill=\\"currentColor\\" viewBox=\\"0 0 20 20\\" xmlns=\\"http://www.w3.org/2000/svg\\">
|
|
||||||
<path fill-rule=\\"evenodd\\" d=\\"M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z\\" clip-rule=\\"evenodd\\"></path>
|
|
||||||
</svg></button>
|
|
||||||
<div class=\\"justify-between items-center w-full md:flex md:w-auto md:order-1 hidden\\" id=\\"mobile-menu-2\\">
|
|
||||||
<ul class=\\"flex flex-col md:flex-row md:space-x-8 mt-2 md:mt-0 md:font-lightbold\\">
|
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
<li>
|
|
||||||
<router-link to=\\"[object Object]\\" class=\\"block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700\\">Login</router-link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<router-link to=\\"[object Object]\\" class=\\"block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700\\">Register</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<!-- <o-navbar
|
|
||||||
id=\\"navbar\\"
|
|
||||||
type=\\"is-secondary\\"
|
|
||||||
wrapper-class=\\"container mx-auto\\"
|
|
||||||
v-model:active=\\"mobileNavbarActive\\"
|
|
||||||
>
|
|
||||||
<template #brand>
|
|
||||||
<o-navbar-item
|
|
||||||
tag=\\"router-link\\"
|
|
||||||
:to=\\"{ name: RouteName.HOME }\\"
|
|
||||||
:aria-label=\\"$t('Home')\\"
|
|
||||||
>
|
|
||||||
<logo />
|
|
||||||
</o-navbar-item>
|
|
||||||
</template>
|
|
||||||
<template #start>
|
|
||||||
<o-navbar-item tag=\\"router-link\\" :to=\\"{ name: RouteName.SEARCH }\\">{{
|
|
||||||
$t(\\"Explore\\")
|
|
||||||
}}</o-navbar-item>
|
|
||||||
<o-navbar-item
|
|
||||||
v-if=\\"currentActor.id && currentUser?.isLoggedIn\\"
|
|
||||||
tag=\\"router-link\\"
|
|
||||||
:to=\\"{ name: RouteName.MY_EVENTS }\\"
|
|
||||||
>{{ $t(\\"My events\\") }}</o-navbar-item
|
|
||||||
>
|
|
||||||
<o-navbar-item
|
|
||||||
tag=\\"router-link\\"
|
|
||||||
:to=\\"{ name: RouteName.MY_GROUPS }\\"
|
|
||||||
v-if=\\"
|
|
||||||
config &&
|
|
||||||
config.features.groups &&
|
|
||||||
currentActor.id &&
|
|
||||||
currentUser?.isLoggedIn
|
|
||||||
\\"
|
|
||||||
>{{ $t(\\"My groups\\") }}</o-navbar-item
|
|
||||||
>
|
|
||||||
<o-navbar-item
|
|
||||||
tag=\\"span\\"
|
|
||||||
v-if=\\"
|
|
||||||
config &&
|
|
||||||
config.features.eventCreation &&
|
|
||||||
currentActor.id &&
|
|
||||||
currentUser?.isLoggedIn
|
|
||||||
\\"
|
|
||||||
>
|
|
||||||
<o-button
|
|
||||||
v-if=\\"!hideCreateEventsButton\\"
|
|
||||||
tag=\\"router-link\\"
|
|
||||||
:to=\\"{ name: RouteName.CREATE_EVENT }\\"
|
|
||||||
variant=\\"primary\\"
|
|
||||||
>{{ $t(\\"Create\\") }}</o-button
|
|
||||||
>
|
|
||||||
</o-navbar-item>
|
|
||||||
</template>
|
|
||||||
<template #end>
|
|
||||||
<o-navbar-item tag=\\"div\\">
|
|
||||||
<search-field @navbar-search=\\"mobileNavbarActive = false\\" />
|
|
||||||
</o-navbar-item>
|
|
||||||
|
|
||||||
<o-navbar-dropdown
|
|
||||||
v-if=\\"currentActor.id && currentUser?.isLoggedIn\\"
|
|
||||||
right
|
|
||||||
collapsible
|
|
||||||
ref=\\"user-dropdown\\"
|
|
||||||
tabindex=\\"0\\"
|
|
||||||
tag=\\"span\\"
|
|
||||||
@keyup.enter=\\"toggleMenu\\"
|
|
||||||
>
|
|
||||||
<template #label v-if=\\"currentActor\\">
|
|
||||||
<div class=\\"identity-wrapper\\">
|
|
||||||
<div>
|
|
||||||
<figure class=\\"image is-32x32\\" v-if=\\"currentActor.avatar\\">
|
|
||||||
<img
|
|
||||||
class=\\"is-rounded\\"
|
|
||||||
alt=\\"avatarUrl\\"
|
|
||||||
:src=\\"currentActor.avatar.url\\"
|
|
||||||
/>
|
|
||||||
</figure>
|
|
||||||
<o-icon v-else icon=\\"account-circle\\" />
|
|
||||||
</div>
|
|
||||||
<div class=\\"media-content is-hidden-desktop\\">
|
|
||||||
<span>{{ displayName(currentActor) }}</span>
|
|
||||||
<span class=\\"has-text-grey-dark\\" v-if=\\"currentActor.name\\"
|
|
||||||
>@{{ currentActor.preferredUsername }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
No identities dropdown if no identities
|
|
||||||
<span v-if=\\"identities.length <= 1\\"></span>
|
|
||||||
<o-navbar-item
|
|
||||||
tag=\\"span\\"
|
|
||||||
v-for=\\"identity in identities\\"
|
|
||||||
v-else
|
|
||||||
:active=\\"identity.id === currentActor.id\\"
|
|
||||||
:key=\\"identity.id\\"
|
|
||||||
tabindex=\\"0\\"
|
|
||||||
@click=\\"setIdentity({
|
|
||||||
preferredUsername: identity.preferredUsername,
|
|
||||||
})\\"
|
|
||||||
@keyup.enter=\\"setIdentity({
|
|
||||||
preferredUsername: identity.preferredUsername,
|
|
||||||
})\\"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<div class=\\"media-left\\">
|
|
||||||
<figure class=\\"image is-32x32\\" v-if=\\"identity.avatar\\">
|
|
||||||
<img
|
|
||||||
class=\\"is-rounded\\"
|
|
||||||
loading=\\"lazy\\"
|
|
||||||
:src=\\"identity.avatar.url\\"
|
|
||||||
alt
|
|
||||||
/>
|
|
||||||
</figure>
|
|
||||||
<o-icon v-else size=\\"is-medium\\" icon=\\"account-circle\\" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=\\"media-content\\">
|
|
||||||
<span>{{ displayName(identity) }}</span>
|
|
||||||
<span class=\\"has-text-grey-dark\\" v-if=\\"identity.name\\"
|
|
||||||
>@{{ identity.preferredUsername }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<hr class=\\"navbar-divider\\" role=\\"presentation\\" />
|
|
||||||
</o-navbar-item>
|
|
||||||
|
|
||||||
<o-navbar-item
|
|
||||||
tag=\\"router-link\\"
|
|
||||||
:to=\\"{ name: RouteName.UPDATE_IDENTITY }\\"
|
|
||||||
>{{ $t(\\"My account\\") }}</o-navbar-item
|
|
||||||
>
|
|
||||||
<o-navbar-item
|
|
||||||
v-if=\\"currentUser.role === ICurrentUserRole.ADMINISTRATOR\\"
|
|
||||||
tag=\\"router-link\\"
|
|
||||||
:to=\\"{ name: RouteName.ADMIN_DASHBOARD }\\"
|
|
||||||
>{{ $t(\\"Administration\\") }}</o-navbar-item
|
|
||||||
>
|
|
||||||
|
|
||||||
<o-navbar-item
|
|
||||||
tag=\\"span\\"
|
|
||||||
tabindex=\\"0\\"
|
|
||||||
@click=\\"logout\\"
|
|
||||||
@keyup.enter=\\"logout\\"
|
|
||||||
>
|
|
||||||
<span>{{ $t(\\"Log out\\") }}</span>
|
|
||||||
</o-navbar-item>
|
|
||||||
</o-navbar-dropdown>
|
|
||||||
|
|
||||||
<o-navbar-item v-else tag=\\"div\\">
|
|
||||||
<div class=\\"buttons\\">
|
|
||||||
<router-link
|
|
||||||
class=\\"button is-primary\\"
|
|
||||||
v-if=\\"config && config.registrationsOpen\\"
|
|
||||||
:to=\\"{ name: RouteName.REGISTER }\\"
|
|
||||||
>
|
|
||||||
<strong>{{ $t(\\"Sign up\\") }}</strong>
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<router-link
|
|
||||||
class=\\"button is-light\\"
|
|
||||||
:to=\\"{ name: RouteName.LOGIN }\\"
|
|
||||||
>{{ $t(\\"Log in\\") }}</router-link
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</o-navbar-item>
|
|
||||||
</template>
|
|
||||||
</o-navbar> -->"
|
|
||||||
`;
|
|
|
@ -1,40 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "esnext",
|
|
||||||
"module": "esnext",
|
|
||||||
"strict": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"importHelpers": true,
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"types": ["webpack-env", "jest", "vite/client", "vite-plugin-pwa/vue"],
|
|
||||||
"typeRoots": ["./@types", "./node_modules/@types"],
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["src/*"]
|
|
||||||
},
|
|
||||||
"lib": [
|
|
||||||
"esnext",
|
|
||||||
"dom",
|
|
||||||
"es2017.intl",
|
|
||||||
"dom.iterable",
|
|
||||||
"scripthost",
|
|
||||||
"webworker"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*.ts",
|
|
||||||
"src/**/*.tsx",
|
|
||||||
"src/**/*.vue",
|
|
||||||
"tests/**/*.ts",
|
|
||||||
"tests/**/*.tsx",
|
|
||||||
"env.d.ts"
|
|
||||||
],
|
|
||||||
"exclude": ["node_modules"]
|
|
||||||
}
|
|
7778
js/yarn.lock
7778
js/yarn.lock
File diff suppressed because it is too large
Load diff
|
@ -14,7 +14,15 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Create do
|
||||||
]
|
]
|
||||||
|
|
||||||
@type create_entities ::
|
@type create_entities ::
|
||||||
:event | :comment | :discussion | :actor | :todo_list | :todo | :resource | :post
|
:event
|
||||||
|
| :comment
|
||||||
|
| :discussion
|
||||||
|
| :conversation
|
||||||
|
| :actor
|
||||||
|
| :todo_list
|
||||||
|
| :todo
|
||||||
|
| :resource
|
||||||
|
| :post
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Create an activity of type `Create`
|
Create an activity of type `Create`
|
||||||
|
@ -50,18 +58,27 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Create do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@map_types %{
|
||||||
|
:event => Types.Events,
|
||||||
|
:comment => Types.Comments,
|
||||||
|
:discussion => Types.Discussions,
|
||||||
|
:conversation => Types.Conversations,
|
||||||
|
:actor => Types.Actors,
|
||||||
|
:todo_list => Types.TodoLists,
|
||||||
|
:todo => Types.Todos,
|
||||||
|
:resource => Types.Resources,
|
||||||
|
:post => Types.Posts
|
||||||
|
}
|
||||||
|
|
||||||
@spec do_create(create_entities(), map(), map()) ::
|
@spec do_create(create_entities(), map(), map()) ::
|
||||||
{:ok, Entity.t(), Activity.t()} | {:error, Ecto.Changeset.t() | atom()}
|
{:ok, Entity.t(), Activity.t()} | {:error, Ecto.Changeset.t() | atom()}
|
||||||
defp do_create(type, args, additional) do
|
defp do_create(type, args, additional) do
|
||||||
case type do
|
mod = Map.get(@map_types, type)
|
||||||
:event -> Types.Events.create(args, additional)
|
|
||||||
:comment -> Types.Comments.create(args, additional)
|
if is_nil(mod) do
|
||||||
:discussion -> Types.Discussions.create(args, additional)
|
{:error, :type_not_supported}
|
||||||
:actor -> Types.Actors.create(args, additional)
|
else
|
||||||
:todo_list -> Types.TodoLists.create(args, additional)
|
mod.create(args, additional)
|
||||||
:todo -> Types.Todos.create(args, additional)
|
|
||||||
:resource -> Types.Resources.create(args, additional)
|
|
||||||
:post -> Types.Posts.create(args, additional)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
||||||
|
|
||||||
alias Mobilizon.{Actors, Discussions, Events, Share}
|
alias Mobilizon.{Actors, Discussions, Events, Share}
|
||||||
alias Mobilizon.Actors.{Actor, Member}
|
alias Mobilizon.Actors.{Actor, Member}
|
||||||
|
alias Mobilizon.Conversations.Conversation
|
||||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||||
alias Mobilizon.Events.{Event, Participant}
|
alias Mobilizon.Events.{Event, Participant}
|
||||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||||
|
@ -38,6 +39,10 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
||||||
%{"to" => maybe_add_group_members([], actor), "cc" => []}
|
%{"to" => maybe_add_group_members([], actor), "cc" => []}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_audience(%Conversation{participants: participants}) do
|
||||||
|
%{"to" => Enum.map(participants, & &1.url), "cc" => []}
|
||||||
|
end
|
||||||
|
|
||||||
# Deleted comments are just like tombstones
|
# Deleted comments are just like tombstones
|
||||||
def get_audience(%Comment{deleted_at: deleted_at}) when not is_nil(deleted_at) do
|
def get_audience(%Comment{deleted_at: deleted_at}) when not is_nil(deleted_at) do
|
||||||
%{"to" => [@ap_public], "cc" => []}
|
%{"to" => [@ap_public], "cc" => []}
|
||||||
|
|
|
@ -177,7 +177,7 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||||
{:error, :content_not_json}
|
{:error, :content_not_json}
|
||||||
|
|
||||||
{:ok, %Tesla.Env{} = res} ->
|
{:ok, %Tesla.Env{} = res} ->
|
||||||
Logger.debug("Resource returned bad HTTP code #{inspect(res)}")
|
Logger.debug("Resource returned bad HTTP code (#{res.status}) #{inspect(res)}")
|
||||||
{:error, :http_error}
|
{:error, :http_error}
|
||||||
|
|
||||||
{:error, err} ->
|
{:error, err} ->
|
||||||
|
|
|
@ -68,24 +68,26 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||||
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object}) do
|
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object}) do
|
||||||
Logger.info("Handle incoming to create notes")
|
Logger.info("Handle incoming to create notes")
|
||||||
|
|
||||||
case Converter.Comment.as_to_model_data(object) do
|
case Discussions.get_comment_from_url_with_preload(object["id"]) do
|
||||||
%{visibility: visibility, event_id: event_id}
|
{:error, :comment_not_found} ->
|
||||||
when visibility != :public and event_id != nil ->
|
case Converter.Comment.as_to_model_data(object) do
|
||||||
Logger.info("Tried to reply to an event with a private comment - ignore")
|
%{visibility: visibility, attributed_to_id: attributed_to_id} = object_data
|
||||||
:error
|
when visibility === :private and is_nil(attributed_to_id) ->
|
||||||
|
Actions.Create.create(:conversation, object_data, false)
|
||||||
|
|
||||||
object_data when is_map(object_data) ->
|
object_data when is_map(object_data) ->
|
||||||
case Discussions.get_comment_from_url_with_preload(object_data.url) do
|
case Discussions.get_comment_from_url_with_preload(object_data.url) do
|
||||||
{:error, :comment_not_found} ->
|
{:error, :comment_not_found} ->
|
||||||
object_data
|
object_data
|
||||||
|> transform_object_data_for_discussion()
|
|> transform_object_data_for_discussion()
|
||||||
|> save_comment_or_discussion()
|
|> save_comment_or_discussion()
|
||||||
|
end
|
||||||
{:ok, %Comment{} = comment} ->
|
|
||||||
# Object already exists
|
|
||||||
{:ok, nil, comment}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
{:ok, %Comment{} = comment} ->
|
||||||
|
# Object already exists
|
||||||
|
{:ok, nil, comment}
|
||||||
|
|
||||||
{:error, err} ->
|
{:error, err} ->
|
||||||
{:error, err}
|
{:error, err}
|
||||||
end
|
end
|
||||||
|
|
219
lib/federation/activity_pub/types/conversation.ex
Normal file
219
lib/federation/activity_pub/types/conversation.ex
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
defmodule Mobilizon.Federation.ActivityPub.Types.Conversations do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
# alias Mobilizon.Conversations.ConversationParticipant
|
||||||
|
alias Mobilizon.{Actors, Conversations, Discussions}
|
||||||
|
alias Mobilizon.Actors.Actor
|
||||||
|
alias Mobilizon.Conversations.Conversation
|
||||||
|
alias Mobilizon.Discussions.Comment
|
||||||
|
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||||
|
alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
|
||||||
|
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||||
|
alias Mobilizon.Federation.ActivityStream
|
||||||
|
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
|
||||||
|
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||||
|
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
||||||
|
alias Mobilizon.Service.Activity.Conversation, as: ConversationActivity
|
||||||
|
alias Mobilizon.Web.Endpoint
|
||||||
|
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@behaviour Entity
|
||||||
|
|
||||||
|
@impl Entity
|
||||||
|
@spec create(map(), map()) ::
|
||||||
|
{:ok, Conversation.t(), ActivityStream.t()}
|
||||||
|
| {:error,
|
||||||
|
:conversation_not_found
|
||||||
|
| :last_comment_not_found
|
||||||
|
| :empty_participants
|
||||||
|
| Ecto.Changeset.t()}
|
||||||
|
def create(%{conversation_id: conversation_id} = args, additional)
|
||||||
|
when not is_nil(conversation_id) do
|
||||||
|
Logger.debug("Creating a reply to a conversation #{inspect(args, pretty: true)}")
|
||||||
|
args = prepare_args(args)
|
||||||
|
Logger.debug("Creating a reply to a conversation #{inspect(args, pretty: true)}")
|
||||||
|
|
||||||
|
with args when is_map(args) <- prepare_args(args) do
|
||||||
|
case Conversations.get_conversation(conversation_id) do
|
||||||
|
%Conversation{} = conversation ->
|
||||||
|
case Conversations.reply_to_conversation(conversation, args) do
|
||||||
|
{:ok, %Conversation{last_comment_id: last_comment_id} = conversation} ->
|
||||||
|
ConversationActivity.insert_activity(conversation, subject: "conversation_replied")
|
||||||
|
maybe_publish_graphql_subscription(conversation)
|
||||||
|
|
||||||
|
case Discussions.get_comment_with_preload(last_comment_id) do
|
||||||
|
%Comment{} = last_comment ->
|
||||||
|
comment_as_data = Convertible.model_to_as(last_comment)
|
||||||
|
audience = Audience.get_audience(conversation)
|
||||||
|
create_data = make_create_data(comment_as_data, Map.merge(audience, additional))
|
||||||
|
{:ok, conversation, create_data}
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
{:error, :last_comment_not_found}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, _, %Ecto.Changeset{} = err, _} ->
|
||||||
|
{:error, err}
|
||||||
|
end
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
{:error, :discussion_not_found}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl Entity
|
||||||
|
def create(args, additional) do
|
||||||
|
with args when is_map(args) <- prepare_args(args) do
|
||||||
|
case Conversations.create_conversation(args) do
|
||||||
|
{:ok, %Conversation{} = conversation} ->
|
||||||
|
ConversationActivity.insert_activity(conversation, subject: "conversation_created")
|
||||||
|
conversation_as_data = Convertible.model_to_as(conversation)
|
||||||
|
audience = Audience.get_audience(conversation)
|
||||||
|
create_data = make_create_data(conversation_as_data, Map.merge(audience, additional))
|
||||||
|
{:ok, conversation, create_data}
|
||||||
|
|
||||||
|
{:error, _, %Ecto.Changeset{} = err, _} ->
|
||||||
|
{:error, err}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl Entity
|
||||||
|
@spec update(Conversation.t(), map(), map()) ::
|
||||||
|
{:ok, Conversation.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
||||||
|
def update(%Conversation{} = old_conversation, args, additional) do
|
||||||
|
case Conversations.update_conversation(old_conversation, args) do
|
||||||
|
{:ok, %Conversation{} = new_conversation} ->
|
||||||
|
# ConversationActivity.insert_activity(new_conversation,
|
||||||
|
# subject: "conversation_renamed",
|
||||||
|
# old_conversation: old_conversation
|
||||||
|
# )
|
||||||
|
|
||||||
|
conversation_as_data = Convertible.model_to_as(new_conversation)
|
||||||
|
audience = Audience.get_audience(new_conversation)
|
||||||
|
update_data = make_update_data(conversation_as_data, Map.merge(audience, additional))
|
||||||
|
{:ok, new_conversation, update_data}
|
||||||
|
|
||||||
|
{:error, %Ecto.Changeset{} = err} ->
|
||||||
|
{:error, err}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl Entity
|
||||||
|
@spec delete(Conversation.t(), Actor.t(), boolean, map()) ::
|
||||||
|
{:error, Ecto.Changeset.t()} | {:ok, ActivityStream.t(), Actor.t(), Conversation.t()}
|
||||||
|
def delete(
|
||||||
|
%Conversation{} = _conversation,
|
||||||
|
%Actor{} = _actor,
|
||||||
|
_local,
|
||||||
|
_additionnal
|
||||||
|
) do
|
||||||
|
{:error, :not_applicable}
|
||||||
|
end
|
||||||
|
|
||||||
|
# @spec actor(Conversation.t()) :: Actor.t() | nil
|
||||||
|
# def actor(%ConversationParticipant{actor_id: actor_id}), do: Actors.get_actor(actor_id)
|
||||||
|
|
||||||
|
# @spec group_actor(Conversation.t()) :: Actor.t() | nil
|
||||||
|
# def group_actor(%Conversation{actor_id: actor_id}), do: Actors.get_actor(actor_id)
|
||||||
|
|
||||||
|
@spec permissions(Conversation.t()) :: Permission.t()
|
||||||
|
def permissions(%Conversation{}) do
|
||||||
|
%Permission{access: :member, create: :member, update: :moderator, delete: :moderator}
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec maybe_publish_graphql_subscription(Conversation.t()) :: :ok
|
||||||
|
defp maybe_publish_graphql_subscription(%Conversation{} = conversation) do
|
||||||
|
Absinthe.Subscription.publish(Endpoint, conversation,
|
||||||
|
conversation_comment_changed: conversation.id
|
||||||
|
)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec prepare_args(map) :: map | {:error, :empty_participants}
|
||||||
|
defp prepare_args(args) do
|
||||||
|
{text, mentions, _tags} =
|
||||||
|
APIUtils.make_content_html(
|
||||||
|
args |> Map.get(:text, "") |> String.trim(),
|
||||||
|
# Can't put additional tags on a comment
|
||||||
|
[],
|
||||||
|
"text/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
mentions =
|
||||||
|
(args |> Map.get(:mentions, []) |> prepare_mentions()) ++
|
||||||
|
ConverterUtils.fetch_mentions(mentions)
|
||||||
|
|
||||||
|
# Can't create a conversation with just ourselves
|
||||||
|
mentions =
|
||||||
|
Enum.filter(mentions, fn %{actor_id: actor_id} ->
|
||||||
|
to_string(actor_id) != to_string(args.actor_id)
|
||||||
|
end)
|
||||||
|
|
||||||
|
if Enum.empty?(mentions) do
|
||||||
|
{:error, :empty_participants}
|
||||||
|
else
|
||||||
|
event = Map.get(args, :event, get_event(Map.get(args, :event_id)))
|
||||||
|
|
||||||
|
participants =
|
||||||
|
(mentions ++
|
||||||
|
[
|
||||||
|
%{actor_id: args.actor_id},
|
||||||
|
%{
|
||||||
|
actor_id:
|
||||||
|
if(is_nil(event),
|
||||||
|
do: nil,
|
||||||
|
else: event.attributed_to_id || event.organizer_actor_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|> Enum.reduce(
|
||||||
|
[],
|
||||||
|
fn %{actor_id: actor_id}, acc ->
|
||||||
|
case Actors.get_actor(actor_id) do
|
||||||
|
nil -> acc
|
||||||
|
actor -> acc ++ [actor]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|> Enum.uniq_by(& &1.id)
|
||||||
|
|
||||||
|
args
|
||||||
|
|> Map.put(:text, text)
|
||||||
|
|> Map.put(:mentions, mentions)
|
||||||
|
|> Map.put(:participants, participants)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec prepare_mentions(list(String.t())) :: list(%{actor_id: String.t()})
|
||||||
|
defp prepare_mentions(mentions) do
|
||||||
|
Enum.reduce(mentions, [], &prepare_mention/2)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec prepare_mention(String.t() | map(), list()) :: list(%{actor_id: String.t()})
|
||||||
|
defp prepare_mention(%{actor_id: _} = mention, mentions) do
|
||||||
|
mentions ++ [mention]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp prepare_mention(mention, mentions) do
|
||||||
|
case ActivityPubActor.find_or_make_actor_from_nickname(mention) do
|
||||||
|
{:ok, %Actor{id: actor_id}} ->
|
||||||
|
mentions ++ [%{actor_id: actor_id}]
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
mentions
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_event(nil), do: nil
|
||||||
|
|
||||||
|
defp get_event(event_id) do
|
||||||
|
case Mobilizon.Events.get_event(event_id) do
|
||||||
|
{:ok, event} -> event
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -22,6 +22,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||||
|
|
||||||
@actor_types ["Group", "Person", "Application"]
|
@actor_types ["Group", "Person", "Application"]
|
||||||
@all_actor_types @actor_types ++ ["Organization", "Service"]
|
@all_actor_types @actor_types ++ ["Organization", "Service"]
|
||||||
|
@ap_public_audience "https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
|
||||||
# Wraps an object into an activity
|
# Wraps an object into an activity
|
||||||
@spec create_activity(map(), boolean()) :: {:ok, Activity.t()}
|
@spec create_activity(map(), boolean()) :: {:ok, Activity.t()}
|
||||||
|
@ -491,8 +492,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||||
if public do
|
if public do
|
||||||
Logger.debug("Making announce data for a public object")
|
Logger.debug("Making announce data for a public object")
|
||||||
|
|
||||||
{[actor.followers_url, object_actor_url],
|
{[actor.followers_url, object_actor_url], [@ap_public_audience]}
|
||||||
["https://www.w3.org/ns/activitystreams#Public"]}
|
|
||||||
else
|
else
|
||||||
Logger.debug("Making announce data for a private object")
|
Logger.debug("Making announce data for a private object")
|
||||||
|
|
||||||
|
@ -539,7 +539,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||||
"actor" => url,
|
"actor" => url,
|
||||||
"object" => activity,
|
"object" => activity,
|
||||||
"to" => [actor.followers_url, actor.url],
|
"to" => [actor.followers_url, actor.url],
|
||||||
"cc" => ["https://www.w3.org/ns/activitystreams#Public"]
|
"cc" => [@ap_public_audience]
|
||||||
}
|
}
|
||||||
|
|
||||||
if activity_id, do: Map.put(data, "id", activity_id), else: data
|
if activity_id, do: Map.put(data, "id", activity_id), else: data
|
||||||
|
|
|
@ -47,9 +47,6 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||||
|
|
||||||
case maybe_fetch_actor_and_attributed_to_id(object) do
|
case maybe_fetch_actor_and_attributed_to_id(object) do
|
||||||
{:ok, %Actor{id: actor_id, domain: actor_domain}, attributed_to} ->
|
{:ok, %Actor{id: actor_id, domain: actor_domain}, attributed_to} ->
|
||||||
Logger.debug("Inserting full comment")
|
|
||||||
Logger.debug(inspect(object))
|
|
||||||
|
|
||||||
data = %{
|
data = %{
|
||||||
text: object["content"],
|
text: object["content"],
|
||||||
url: object["id"],
|
url: object["id"],
|
||||||
|
@ -70,14 +67,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||||
is_announcement: Map.get(object, "isAnnouncement", false)
|
is_announcement: Map.get(object, "isAnnouncement", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.debug("Converted object before fetching parents")
|
maybe_fetch_parent_object(object, data)
|
||||||
Logger.debug(inspect(data))
|
|
||||||
|
|
||||||
data = maybe_fetch_parent_object(object, data)
|
|
||||||
|
|
||||||
Logger.debug("Converted object after fetching parents")
|
|
||||||
Logger.debug(inspect(data))
|
|
||||||
data
|
|
||||||
|
|
||||||
{:error, err} ->
|
{:error, err} ->
|
||||||
{:error, err}
|
{:error, err}
|
||||||
|
@ -147,19 +137,22 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec determine_to(CommentModel.t()) :: [String.t()]
|
@spec determine_to(CommentModel.t()) :: [String.t()]
|
||||||
defp determine_to(%CommentModel{} = comment) do
|
defp determine_to(%CommentModel{visibility: :private, mentions: mentions} = _comment) do
|
||||||
cond do
|
Enum.map(mentions, fn mention -> mention.actor.url end)
|
||||||
not is_nil(comment.attributed_to) ->
|
end
|
||||||
[comment.attributed_to.url]
|
|
||||||
|
|
||||||
comment.visibility == :public ->
|
defp determine_to(%CommentModel{visibility: :public} = comment) do
|
||||||
["https://www.w3.org/ns/activitystreams#Public"]
|
if is_nil(comment.attributed_to) do
|
||||||
|
["https://www.w3.org/ns/activitystreams#Public"]
|
||||||
true ->
|
else
|
||||||
[comment.actor.followers_url]
|
[comment.attributed_to.url]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp determine_to(%CommentModel{} = comment) do
|
||||||
|
[comment.actor.followers_url]
|
||||||
|
end
|
||||||
|
|
||||||
defp maybe_fetch_parent_object(object, data) do
|
defp maybe_fetch_parent_object(object, data) do
|
||||||
# We fetch the parent object
|
# We fetch the parent object
|
||||||
Logger.debug("We're fetching the parent object")
|
Logger.debug("We're fetching the parent object")
|
||||||
|
@ -170,9 +163,12 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||||
|
|
||||||
case ActivityPub.fetch_object_from_url(object["inReplyTo"]) do
|
case ActivityPub.fetch_object_from_url(object["inReplyTo"]) do
|
||||||
# Reply to an event (Event)
|
# Reply to an event (Event)
|
||||||
{:ok, %Event{id: id}} ->
|
{:ok, %Event{id: id} = event} ->
|
||||||
Logger.debug("Parent object is an event")
|
Logger.debug("Parent object is an event")
|
||||||
data |> Map.put(:event_id, id)
|
|
||||||
|
data
|
||||||
|
|> Map.put(:event_id, id)
|
||||||
|
|> Map.put(:event, event)
|
||||||
|
|
||||||
# Reply to a comment (Comment)
|
# Reply to a comment (Comment)
|
||||||
{:ok, %CommentModel{id: id} = comment} ->
|
{:ok, %CommentModel{id: id} = comment} ->
|
||||||
|
@ -182,6 +178,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||||
|> Map.put(:in_reply_to_comment_id, id)
|
|> Map.put(:in_reply_to_comment_id, id)
|
||||||
|> Map.put(:origin_comment_id, comment |> CommentModel.get_thread_id())
|
|> Map.put(:origin_comment_id, comment |> CommentModel.get_thread_id())
|
||||||
|> Map.put(:event_id, comment.event_id)
|
|> Map.put(:event_id, comment.event_id)
|
||||||
|
|> Map.put(:conversation_id, comment.conversation_id)
|
||||||
|
|
||||||
# Reply to a discucssion (Discussion)
|
# Reply to a discucssion (Discussion)
|
||||||
{:ok,
|
{:ok,
|
||||||
|
|
68
lib/federation/activity_stream/converter/conversation.ex
Normal file
68
lib/federation/activity_stream/converter/conversation.ex
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
defmodule Mobilizon.Federation.ActivityStream.Converter.Conversation do
|
||||||
|
@moduledoc """
|
||||||
|
Comment converter.
|
||||||
|
|
||||||
|
This module allows to convert conversations from ActivityStream format to our own
|
||||||
|
internal one, and back.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Mobilizon.Actors.Actor
|
||||||
|
alias Mobilizon.Conversations.Conversation
|
||||||
|
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||||
|
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
|
||||||
|
alias Mobilizon.Federation.ActivityStream.Converter.Conversation, as: ConversationConverter
|
||||||
|
alias Mobilizon.Storage.Repo
|
||||||
|
import Mobilizon.Service.Guards, only: [is_valid_string: 1]
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@behaviour Converter
|
||||||
|
|
||||||
|
defimpl Convertible, for: Conversation do
|
||||||
|
defdelegate model_to_as(comment), to: ConversationConverter
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Make an AS comment object from an existing `conversation` structure.
|
||||||
|
"""
|
||||||
|
@impl Converter
|
||||||
|
@spec model_to_as(Conversation.t()) :: map
|
||||||
|
def model_to_as(%Conversation{} = conversation) do
|
||||||
|
conversation = Repo.preload(conversation, [:participants, last_comment: [:actor]])
|
||||||
|
|
||||||
|
%{
|
||||||
|
"type" => "Note",
|
||||||
|
"to" => Enum.map(conversation.participants, & &1.url),
|
||||||
|
"cc" => [],
|
||||||
|
"content" => conversation.last_comment.text,
|
||||||
|
"mediaType" => "text/html",
|
||||||
|
"actor" => conversation.last_comment.actor.url,
|
||||||
|
"id" => conversation.last_comment.url,
|
||||||
|
"publishedAt" => conversation.inserted_at
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl Converter
|
||||||
|
@spec as_to_model_data(map) :: map() | {:error, atom()}
|
||||||
|
def as_to_model_data(%{"type" => "Note", "name" => name} = object) when is_valid_string(name) do
|
||||||
|
with %{actor_id: actor_id, creator_id: creator_id} <- extract_actors(object) do
|
||||||
|
%{actor_id: actor_id, creator_id: creator_id, title: name, url: object["id"]}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec extract_actors(map()) ::
|
||||||
|
%{actor_id: String.t(), creator_id: String.t()} | {:error, atom()}
|
||||||
|
defp extract_actors(%{"actor" => creator_url, "attributedTo" => actor_url} = _object)
|
||||||
|
when is_valid_string(creator_url) and is_valid_string(actor_url) do
|
||||||
|
with {:ok, %Actor{id: creator_id, suspended: false}} <-
|
||||||
|
ActivityPubActor.get_or_fetch_actor_by_url(creator_url),
|
||||||
|
{:ok, %Actor{id: actor_id, suspended: false}} <-
|
||||||
|
ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do
|
||||||
|
%{actor_id: actor_id, creator_id: creator_id}
|
||||||
|
else
|
||||||
|
{:error, error} -> {:error, error}
|
||||||
|
{:ok, %Actor{url: ^creator_url}} -> {:error, :creator_suspended}
|
||||||
|
{:ok, %Actor{url: ^actor_url}} -> {:error, :actor_suspended}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -242,12 +242,15 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||||
@spec domain_from_federated_actor(String.t()) :: {:ok, String.t()} | {:error, :host_not_found}
|
@spec domain_from_federated_actor(String.t()) :: {:ok, String.t()} | {:error, :host_not_found}
|
||||||
defp domain_from_federated_actor(actor) do
|
defp domain_from_federated_actor(actor) do
|
||||||
case String.split(actor, "@") do
|
case String.split(actor, "@") do
|
||||||
|
[_name, ""] ->
|
||||||
|
{:error, :host_not_found}
|
||||||
|
|
||||||
[_name, domain] ->
|
[_name, domain] ->
|
||||||
{:ok, domain}
|
{:ok, domain}
|
||||||
|
|
||||||
_e ->
|
_e ->
|
||||||
host = URI.parse(actor).host
|
host = URI.parse(actor).host
|
||||||
if is_nil(host), do: {:error, :host_not_found}, else: {:ok, host}
|
if is_nil(host) or host == "", do: {:error, :host_not_found}, else: {:ok, host}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,8 @@ defmodule Mobilizon.GraphQL.API.Comments do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Discussions.Comment
|
alias Mobilizon.Conversations.Conversation
|
||||||
|
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||||
alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
|
alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
|
||||||
alias Mobilizon.GraphQL.API.Utils
|
alias Mobilizon.GraphQL.API.Utils
|
||||||
|
|
||||||
|
@ -53,6 +54,22 @@ defmodule Mobilizon.GraphQL.API.Comments do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a conversation (or reply to a conversation)
|
||||||
|
"""
|
||||||
|
@spec create_conversation(map()) ::
|
||||||
|
{:ok, Activity.t(), Conversation.t()}
|
||||||
|
| {:error, :entity_tombstoned | atom | Ecto.Changeset.t()}
|
||||||
|
def create_conversation(args) do
|
||||||
|
args = extract_pictures_from_comment_body(args)
|
||||||
|
|
||||||
|
Actions.Create.create(
|
||||||
|
:conversation,
|
||||||
|
args,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
@spec extract_pictures_from_comment_body(map()) :: map()
|
@spec extract_pictures_from_comment_body(map()) :: map()
|
||||||
defp extract_pictures_from_comment_body(%{text: text, actor_id: actor_id} = args) do
|
defp extract_pictures_from_comment_body(%{text: text, actor_id: actor_id} = args) do
|
||||||
pictures = Utils.extract_pictures_from_body(text, actor_id)
|
pictures = Utils.extract_pictures_from_body(text, actor_id)
|
||||||
|
|
|
@ -4,8 +4,8 @@ defmodule Mobilizon.GraphQL.API.Events do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
|
alias Mobilizon.Discussions.Comment
|
||||||
alias Mobilizon.Events.Event
|
alias Mobilizon.Events.Event
|
||||||
|
|
||||||
alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Utils}
|
alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Utils}
|
||||||
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
||||||
|
|
||||||
|
@ -36,6 +36,12 @@ defmodule Mobilizon.GraphQL.API.Events do
|
||||||
Actions.Delete.delete(event, actor, true)
|
Actions.Delete.delete(event, actor, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec send_private_message_to_participants(map()) ::
|
||||||
|
{:ok, Activity.t(), Comment.t()} | {:error, atom() | Ecto.Changeset.t()}
|
||||||
|
def send_private_message_to_participants(args) do
|
||||||
|
Actions.Create.create(:comment, args, true)
|
||||||
|
end
|
||||||
|
|
||||||
@spec prepare_args(map) :: map
|
@spec prepare_args(map) :: map
|
||||||
defp prepare_args(args) do
|
defp prepare_args(args) do
|
||||||
organizer_actor = Map.get(args, :organizer_actor)
|
organizer_actor = Map.get(args, :organizer_actor)
|
||||||
|
|
|
@ -116,13 +116,9 @@ defmodule Mobilizon.GraphQL.API.Search do
|
||||||
@spec process_from_username(String.t()) :: Page.t(Actor.t())
|
@spec process_from_username(String.t()) :: Page.t(Actor.t())
|
||||||
defp process_from_username(search) do
|
defp process_from_username(search) do
|
||||||
case ActivityPubActor.find_or_make_actor_from_nickname(search) do
|
case ActivityPubActor.find_or_make_actor_from_nickname(search) do
|
||||||
{:ok, %Actor{type: :Group} = actor} ->
|
{:ok, %Actor{} = actor} ->
|
||||||
%Page{total: 1, elements: [actor]}
|
%Page{total: 1, elements: [actor]}
|
||||||
|
|
||||||
# Don't return anything else than groups
|
|
||||||
{:ok, %Actor{}} ->
|
|
||||||
%Page{total: 0, elements: []}
|
|
||||||
|
|
||||||
{:error, _err} ->
|
{:error, _err} ->
|
||||||
Logger.debug(fn -> "Unable to find or make actor '#{search}'" end)
|
Logger.debug(fn -> "Unable to find or make actor '#{search}'" end)
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,13 @@ defmodule Mobilizon.GraphQL.Authorization do
|
||||||
@impl true
|
@impl true
|
||||||
def has_user_access?(%User{}, _scope, _rule), do: true
|
def has_user_access?(%User{}, _scope, _rule), do: true
|
||||||
|
|
||||||
|
@impl true
|
||||||
def has_user_access?(%ApplicationToken{scope: scope} = _current_app_token, _struct, rule)
|
def has_user_access?(%ApplicationToken{scope: scope} = _current_app_token, _struct, rule)
|
||||||
when rule != :forbid_app_access do
|
when rule != :forbid_app_access do
|
||||||
AppScope.has_app_access?(scope, rule)
|
AppScope.has_app_access?(scope, rule)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def has_user_access?(_current_user, _scoped_struct, _rule), do: false
|
def has_user_access?(_current_user, _scoped_struct, _rule), do: false
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
277
lib/graphql/resolvers/conversation.ex
Normal file
277
lib/graphql/resolvers/conversation.ex
Normal file
|
@ -0,0 +1,277 @@
|
||||||
|
defmodule Mobilizon.GraphQL.Resolvers.Conversation do
|
||||||
|
@moduledoc """
|
||||||
|
Handles the group-related GraphQL calls.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Mobilizon.{Actors, Conversations}
|
||||||
|
alias Mobilizon.Actors.Actor
|
||||||
|
alias Mobilizon.Conversations.{Conversation, ConversationParticipant, ConversationView}
|
||||||
|
alias Mobilizon.Events.Event
|
||||||
|
alias Mobilizon.GraphQL.API.Comments
|
||||||
|
alias Mobilizon.Storage.Page
|
||||||
|
alias Mobilizon.Users.User
|
||||||
|
alias Mobilizon.Web.Endpoint
|
||||||
|
import Mobilizon.Web.Gettext, only: [dgettext: 2]
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@spec find_conversations_for_event(Event.t(), map(), Absinthe.Resolution.t()) ::
|
||||||
|
{:ok, Page.t(ConversationView.t())} | {:error, :unauthenticated}
|
||||||
|
def find_conversations_for_event(
|
||||||
|
%Event{id: event_id, attributed_to_id: attributed_to_id},
|
||||||
|
%{page: page, limit: limit},
|
||||||
|
%{
|
||||||
|
context: %{
|
||||||
|
current_actor: %Actor{id: actor_id}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
when not is_nil(attributed_to_id) do
|
||||||
|
if Actors.is_member?(actor_id, attributed_to_id) do
|
||||||
|
{:ok,
|
||||||
|
event_id
|
||||||
|
|> Conversations.find_conversations_for_event(attributed_to_id, page, limit)
|
||||||
|
|> conversation_participant_to_view()}
|
||||||
|
else
|
||||||
|
{:ok, %Page{total: 0, elements: []}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec find_conversations_for_event(Event.t(), map(), Absinthe.Resolution.t()) ::
|
||||||
|
{:ok, Page.t(ConversationView.t())} | {:error, :unauthenticated}
|
||||||
|
def find_conversations_for_event(
|
||||||
|
%Event{id: event_id, organizer_actor_id: organizer_actor_id},
|
||||||
|
%{page: page, limit: limit},
|
||||||
|
%{
|
||||||
|
context: %{
|
||||||
|
current_actor: %Actor{id: actor_id}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) do
|
||||||
|
if organizer_actor_id == actor_id do
|
||||||
|
{:ok,
|
||||||
|
event_id
|
||||||
|
|> Conversations.find_conversations_for_event(actor_id, page, limit)
|
||||||
|
|> conversation_participant_to_view()}
|
||||||
|
else
|
||||||
|
{:ok, %Page{total: 0, elements: []}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_conversations(%Actor{id: actor_id}, %{page: page, limit: limit}, %{
|
||||||
|
context: %{
|
||||||
|
current_actor: %Actor{id: _current_actor_id}
|
||||||
|
}
|
||||||
|
}) do
|
||||||
|
{:ok,
|
||||||
|
actor_id
|
||||||
|
|> Conversations.list_conversation_participants_for_actor(page, limit)
|
||||||
|
|> conversation_participant_to_view()}
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_conversations(%User{id: user_id}, %{page: page, limit: limit}, %{
|
||||||
|
context: %{
|
||||||
|
current_actor: %Actor{id: _current_actor_id}
|
||||||
|
}
|
||||||
|
}) do
|
||||||
|
{:ok,
|
||||||
|
user_id
|
||||||
|
|> Conversations.list_conversation_participants_for_user(page, limit)
|
||||||
|
|> conversation_participant_to_view()}
|
||||||
|
end
|
||||||
|
|
||||||
|
def unread_conversations_count(%Actor{id: actor_id}, _args, %{
|
||||||
|
context: %{
|
||||||
|
current_user: %User{} = user
|
||||||
|
}
|
||||||
|
}) do
|
||||||
|
case User.owns_actor(user, actor_id) do
|
||||||
|
{:is_owned, %Actor{}} ->
|
||||||
|
{:ok, Conversations.count_unread_conversation_participants_for_person(actor_id)}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :unauthorized}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_conversation(_parent, %{id: conversation_participant_id}, %{
|
||||||
|
context: %{
|
||||||
|
current_actor: %Actor{id: performing_actor_id}
|
||||||
|
}
|
||||||
|
}) do
|
||||||
|
case Conversations.get_conversation_participant(conversation_participant_id) do
|
||||||
|
nil ->
|
||||||
|
{:error, :not_found}
|
||||||
|
|
||||||
|
%ConversationParticipant{actor_id: actor_id} = conversation_participant ->
|
||||||
|
if actor_id == performing_actor_id or Actors.is_member?(performing_actor_id, actor_id) do
|
||||||
|
{:ok, conversation_participant_to_view(conversation_participant)}
|
||||||
|
else
|
||||||
|
{:error, :not_found}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_comments_for_conversation(
|
||||||
|
%ConversationView{origin_comment_id: origin_comment_id, actor_id: conversation_actor_id},
|
||||||
|
%{page: page, limit: limit},
|
||||||
|
%{
|
||||||
|
context: %{
|
||||||
|
current_actor: %Actor{id: performing_actor_id}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) do
|
||||||
|
if conversation_actor_id == performing_actor_id or
|
||||||
|
Actors.is_member?(performing_actor_id, conversation_actor_id) do
|
||||||
|
{:ok,
|
||||||
|
Mobilizon.Discussions.get_comments_in_reply_to_comment_id(origin_comment_id, page, limit)}
|
||||||
|
else
|
||||||
|
{:error, :unauthorized}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_conversation(
|
||||||
|
_parent,
|
||||||
|
%{actor_id: actor_id} = args,
|
||||||
|
%{
|
||||||
|
context: %{
|
||||||
|
current_actor: %Actor{} = current_actor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) do
|
||||||
|
if authorized_to_reply?(
|
||||||
|
Map.get(args, :conversation_id),
|
||||||
|
Map.get(args, :attributed_to_id),
|
||||||
|
current_actor.id
|
||||||
|
) do
|
||||||
|
case Comments.create_conversation(args) do
|
||||||
|
{:ok, _activity, %Conversation{} = conversation} ->
|
||||||
|
Absinthe.Subscription.publish(
|
||||||
|
Endpoint,
|
||||||
|
Conversations.count_unread_conversation_participants_for_person(current_actor.id),
|
||||||
|
person_unread_conversations_count: current_actor.id
|
||||||
|
)
|
||||||
|
|
||||||
|
conversation_participant_actor =
|
||||||
|
args |> Map.get(:attributed_to_id, actor_id) |> Actors.get_actor()
|
||||||
|
|
||||||
|
{:ok, conversation_to_view(conversation, conversation_participant_actor)}
|
||||||
|
|
||||||
|
{:error, :empty_participants} ->
|
||||||
|
{:error,
|
||||||
|
dgettext(
|
||||||
|
"errors",
|
||||||
|
"Conversation needs to mention at least one participant that's not yourself"
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Logger.debug(
|
||||||
|
"Actor #{current_actor.id} is not authorized to reply to conversation #{inspect(Map.get(args, :conversation_id))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
{:error, :unauthorized}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_conversation(_parent, %{conversation_id: conversation_participant_id, read: read}, %{
|
||||||
|
context: %{
|
||||||
|
current_actor: %Actor{id: current_actor_id}
|
||||||
|
}
|
||||||
|
}) do
|
||||||
|
with {:no_participant,
|
||||||
|
%ConversationParticipant{actor_id: actor_id} = conversation_participant} <-
|
||||||
|
{:no_participant,
|
||||||
|
Conversations.get_conversation_participant(conversation_participant_id)},
|
||||||
|
{:valid_actor, true} <-
|
||||||
|
{:valid_actor,
|
||||||
|
actor_id == current_actor_id or
|
||||||
|
Actors.is_member?(current_actor_id, actor_id)},
|
||||||
|
{:ok, %ConversationParticipant{} = conversation_participant} <-
|
||||||
|
Conversations.update_conversation_participant(conversation_participant, %{
|
||||||
|
unread: !read
|
||||||
|
}) do
|
||||||
|
Absinthe.Subscription.publish(
|
||||||
|
Endpoint,
|
||||||
|
Conversations.count_unread_conversation_participants_for_person(actor_id),
|
||||||
|
person_unread_conversations_count: actor_id
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, conversation_participant_to_view(conversation_participant)}
|
||||||
|
else
|
||||||
|
{:no_participant, _} ->
|
||||||
|
{:error, :not_found}
|
||||||
|
|
||||||
|
{:valid_actor, _} ->
|
||||||
|
{:error, :unauthorized}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_conversation(_, _, _), do: :ok
|
||||||
|
|
||||||
|
defp conversation_participant_to_view(%Page{elements: elements} = page) do
|
||||||
|
%Page{page | elements: Enum.map(elements, &conversation_participant_to_view/1)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp conversation_participant_to_view(%ConversationParticipant{} = conversation_participant) do
|
||||||
|
value =
|
||||||
|
conversation_participant
|
||||||
|
|> Map.from_struct()
|
||||||
|
|> Map.merge(Map.from_struct(conversation_participant.conversation))
|
||||||
|
|> Map.delete(:conversation)
|
||||||
|
|> Map.put(
|
||||||
|
:participants,
|
||||||
|
Enum.map(
|
||||||
|
conversation_participant.conversation.participants,
|
||||||
|
&conversation_participant_to_actor/1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|> Map.put(:conversation_participant_id, conversation_participant.id)
|
||||||
|
|
||||||
|
struct(ConversationView, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp conversation_to_view(
|
||||||
|
%Conversation{id: conversation_id} = conversation,
|
||||||
|
%Actor{id: actor_id} = actor,
|
||||||
|
unread \\ true
|
||||||
|
) do
|
||||||
|
value =
|
||||||
|
conversation
|
||||||
|
|> Map.from_struct()
|
||||||
|
|> Map.put(:actor, actor)
|
||||||
|
|> Map.put(:unread, unread)
|
||||||
|
|> Map.put(
|
||||||
|
:conversation_participant_id,
|
||||||
|
Conversations.get_participant_by_conversation_and_actor(conversation_id, actor_id).id
|
||||||
|
)
|
||||||
|
|
||||||
|
struct(ConversationView, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp conversation_participant_to_actor(%Actor{} = actor), do: actor
|
||||||
|
|
||||||
|
defp conversation_participant_to_actor(%ConversationParticipant{} = conversation_participant),
|
||||||
|
do: conversation_participant.actor
|
||||||
|
|
||||||
|
@spec authorized_to_reply?(String.t() | nil, String.t() | nil, String.t()) :: boolean()
|
||||||
|
# Not a reply
|
||||||
|
defp authorized_to_reply?(conversation_id, _attributed_to_id, _current_actor_id)
|
||||||
|
when is_nil(conversation_id),
|
||||||
|
do: true
|
||||||
|
|
||||||
|
# We are authorized to reply if we are one of the participants, or if we a a member of a participant group
|
||||||
|
defp authorized_to_reply?(conversation_id, attributed_to_id, current_actor_id) do
|
||||||
|
case Conversations.get_conversation(conversation_id) do
|
||||||
|
nil ->
|
||||||
|
false
|
||||||
|
|
||||||
|
%Conversation{participants: participants} ->
|
||||||
|
participant_ids = Enum.map(participants, fn participant -> to_string(participant.id) end)
|
||||||
|
|
||||||
|
to_string(current_actor_id) in participant_ids or
|
||||||
|
Enum.any?(participant_ids, fn participant_id ->
|
||||||
|
Actors.is_member?(current_actor_id, participant_id) and
|
||||||
|
attributed_to_id == participant_id
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,9 +2,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Handles the participation-related GraphQL calls.
|
Handles the participation-related GraphQL calls.
|
||||||
"""
|
"""
|
||||||
alias Mobilizon.{Actors, Config, Crypto, Events}
|
alias Mobilizon.{Actors, Config, Conversations, Crypto, Events}
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
|
alias Mobilizon.Conversations.{Conversation, ConversationView}
|
||||||
alias Mobilizon.Events.{Event, Participant}
|
alias Mobilizon.Events.{Event, Participant}
|
||||||
|
alias Mobilizon.GraphQL.API.Comments
|
||||||
alias Mobilizon.GraphQL.API.Participations
|
alias Mobilizon.GraphQL.API.Participations
|
||||||
alias Mobilizon.Service.Export.Participants.{CSV, ODS, PDF}
|
alias Mobilizon.Service.Export.Participants.{CSV, ODS, PDF}
|
||||||
alias Mobilizon.Users.User
|
alias Mobilizon.Users.User
|
||||||
|
@ -346,6 +348,75 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
||||||
|
|
||||||
def export_event_participants(_, _, _), do: {:error, :unauthorized}
|
def export_event_participants(_, _, _), do: {:error, :unauthorized}
|
||||||
|
|
||||||
|
def send_private_messages_to_participants(
|
||||||
|
_parent,
|
||||||
|
%{roles: roles, event_id: event_id, actor_id: actor_id} =
|
||||||
|
args,
|
||||||
|
%{
|
||||||
|
context: %{
|
||||||
|
current_user: %User{locale: _locale},
|
||||||
|
current_actor: %Actor{id: current_actor_id}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) do
|
||||||
|
participant_actors =
|
||||||
|
event_id
|
||||||
|
|> Events.list_all_participants_for_event(roles)
|
||||||
|
|> Enum.map(& &1.actor)
|
||||||
|
|
||||||
|
mentions =
|
||||||
|
participant_actors
|
||||||
|
|> Enum.map(& &1.id)
|
||||||
|
|> Enum.uniq()
|
||||||
|
|> Enum.map(&%{actor_id: &1, event_id: event_id})
|
||||||
|
|
||||||
|
args =
|
||||||
|
Map.merge(args, %{
|
||||||
|
mentions: mentions,
|
||||||
|
visibility: :private
|
||||||
|
})
|
||||||
|
|
||||||
|
with {:member, true} <-
|
||||||
|
{:member,
|
||||||
|
current_actor_id == actor_id or Actors.is_member?(current_actor_id, actor_id)},
|
||||||
|
{:ok, _activity, %Conversation{} = conversation} <- Comments.create_conversation(args) do
|
||||||
|
{:ok, conversation_to_view(conversation, Actors.get_actor(actor_id))}
|
||||||
|
else
|
||||||
|
{:member, false} ->
|
||||||
|
{:error, :unauthorized}
|
||||||
|
|
||||||
|
{:error, :empty_participants} ->
|
||||||
|
{:error,
|
||||||
|
dgettext(
|
||||||
|
"errors",
|
||||||
|
"There are no participants matching the audience you've selected."
|
||||||
|
)}
|
||||||
|
|
||||||
|
{:error, err} ->
|
||||||
|
{:error, err}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_private_messages_to_participants(_parent, _args, _resolution),
|
||||||
|
do: {:error, :unauthorized}
|
||||||
|
|
||||||
|
defp conversation_to_view(
|
||||||
|
%Conversation{id: conversation_id} = conversation,
|
||||||
|
%Actor{id: actor_id} = actor
|
||||||
|
) do
|
||||||
|
value =
|
||||||
|
conversation
|
||||||
|
|> Map.from_struct()
|
||||||
|
|> Map.put(:actor, actor)
|
||||||
|
|> Map.put(:unread, false)
|
||||||
|
|> Map.put(
|
||||||
|
:conversation_participant_id,
|
||||||
|
Conversations.get_participant_by_conversation_and_actor(conversation_id, actor_id).id
|
||||||
|
)
|
||||||
|
|
||||||
|
struct(ConversationView, value)
|
||||||
|
end
|
||||||
|
|
||||||
@spec valid_email?(String.t() | nil) :: boolean
|
@spec valid_email?(String.t() | nil) :: boolean
|
||||||
defp valid_email?(email) when is_nil(email), do: false
|
defp valid_email?(email) when is_nil(email), do: false
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ defmodule Mobilizon.GraphQL.Schema do
|
||||||
import_types(Schema.Users.ActivitySetting)
|
import_types(Schema.Users.ActivitySetting)
|
||||||
import_types(Schema.FollowedGroupActivityType)
|
import_types(Schema.FollowedGroupActivityType)
|
||||||
import_types(Schema.AuthApplicationType)
|
import_types(Schema.AuthApplicationType)
|
||||||
|
import_types(Schema.ConversationType)
|
||||||
|
|
||||||
@desc "A struct containing the id of the deleted object"
|
@desc "A struct containing the id of the deleted object"
|
||||||
object :deleted_object do
|
object :deleted_object do
|
||||||
|
@ -165,6 +166,7 @@ defmodule Mobilizon.GraphQL.Schema do
|
||||||
import_fields(:todo_list_queries)
|
import_fields(:todo_list_queries)
|
||||||
import_fields(:todo_queries)
|
import_fields(:todo_queries)
|
||||||
import_fields(:discussion_queries)
|
import_fields(:discussion_queries)
|
||||||
|
import_fields(:conversation_queries)
|
||||||
import_fields(:resource_queries)
|
import_fields(:resource_queries)
|
||||||
import_fields(:post_queries)
|
import_fields(:post_queries)
|
||||||
import_fields(:statistics_queries)
|
import_fields(:statistics_queries)
|
||||||
|
@ -189,6 +191,7 @@ defmodule Mobilizon.GraphQL.Schema do
|
||||||
import_fields(:todo_list_mutations)
|
import_fields(:todo_list_mutations)
|
||||||
import_fields(:todo_mutations)
|
import_fields(:todo_mutations)
|
||||||
import_fields(:discussion_mutations)
|
import_fields(:discussion_mutations)
|
||||||
|
import_fields(:conversation_mutations)
|
||||||
import_fields(:resource_mutations)
|
import_fields(:resource_mutations)
|
||||||
import_fields(:post_mutations)
|
import_fields(:post_mutations)
|
||||||
import_fields(:actor_mutations)
|
import_fields(:actor_mutations)
|
||||||
|
@ -204,6 +207,7 @@ defmodule Mobilizon.GraphQL.Schema do
|
||||||
subscription do
|
subscription do
|
||||||
import_fields(:person_subscriptions)
|
import_fields(:person_subscriptions)
|
||||||
import_fields(:discussion_subscriptions)
|
import_fields(:discussion_subscriptions)
|
||||||
|
import_fields(:conversation_subscriptions)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec middleware(list(module()), any(), map()) :: list(module())
|
@spec middleware(list(module()), any(), map()) :: list(module())
|
||||||
|
|
|
@ -7,7 +7,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||||
import Absinthe.Resolution.Helpers, only: [dataloader: 2]
|
import Absinthe.Resolution.Helpers, only: [dataloader: 2]
|
||||||
|
|
||||||
alias Mobilizon.Events
|
alias Mobilizon.Events
|
||||||
alias Mobilizon.GraphQL.Resolvers.{Media, Person}
|
alias Mobilizon.GraphQL.Resolvers.{Conversation, Media, Person}
|
||||||
alias Mobilizon.GraphQL.Schema
|
alias Mobilizon.GraphQL.Schema
|
||||||
|
|
||||||
import_types(Schema.Events.FeedTokenType)
|
import_types(Schema.Events.FeedTokenType)
|
||||||
|
@ -136,6 +136,25 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||||
arg(:limit, :integer, default_value: 10, description: "The limit of follows per page")
|
arg(:limit, :integer, default_value: 10, description: "The limit of follows per page")
|
||||||
resolve(&Person.person_follows/3)
|
resolve(&Person.person_follows/3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@desc "The list of conversations this person has"
|
||||||
|
field(:conversations, :paginated_conversation_list,
|
||||||
|
meta: [private: true, rule: :"read:profile:conversations"]
|
||||||
|
) do
|
||||||
|
arg(:page, :integer,
|
||||||
|
default_value: 1,
|
||||||
|
description: "The page in the conversations list"
|
||||||
|
)
|
||||||
|
|
||||||
|
arg(:limit, :integer, default_value: 10, description: "The limit of conversations per page")
|
||||||
|
resolve(&Conversation.list_conversations/3)
|
||||||
|
end
|
||||||
|
|
||||||
|
field(:unread_conversations_count, :integer,
|
||||||
|
meta: [private: true, rule: :"read:profile:conversations"]
|
||||||
|
) do
|
||||||
|
resolve(&Conversation.unread_conversations_count/3)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@desc """
|
@desc """
|
||||||
|
@ -353,5 +372,16 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||||
{:ok, topic: [args.group, args.person_id]}
|
{:ok, topic: [args.group, args.person_id]}
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@desc "Notify when a person unread conversations count changed"
|
||||||
|
field(:person_unread_conversations_count, :integer,
|
||||||
|
meta: [private: true, rule: :"read:profile:conversations"]
|
||||||
|
) do
|
||||||
|
arg(:person_id, non_null(:id), description: "The person's ID")
|
||||||
|
|
||||||
|
config(fn args, _ ->
|
||||||
|
{:ok, topic: [args.person_id]}
|
||||||
|
end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
132
lib/graphql/schema/conversation.ex
Normal file
132
lib/graphql/schema/conversation.ex
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
defmodule Mobilizon.GraphQL.Schema.ConversationType do
|
||||||
|
@moduledoc """
|
||||||
|
Schema representation for conversation
|
||||||
|
"""
|
||||||
|
use Absinthe.Schema.Notation
|
||||||
|
|
||||||
|
# import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||||
|
|
||||||
|
# alias Mobilizon.Actors
|
||||||
|
alias Mobilizon.GraphQL.Resolvers.Conversation
|
||||||
|
|
||||||
|
@desc "A conversation"
|
||||||
|
object :conversation do
|
||||||
|
meta(:authorize, :user)
|
||||||
|
interfaces([:activity_object])
|
||||||
|
field(:id, :id, description: "Internal ID for this conversation")
|
||||||
|
|
||||||
|
field(:conversation_participant_id, :id,
|
||||||
|
description: "Internal ID for the conversation participant"
|
||||||
|
)
|
||||||
|
|
||||||
|
field(:last_comment, :comment, description: "The last comment of the conversation")
|
||||||
|
|
||||||
|
field :comments, :paginated_comment_list do
|
||||||
|
arg(:page, :integer, default_value: 1)
|
||||||
|
arg(:limit, :integer, default_value: 10)
|
||||||
|
resolve(&Conversation.get_comments_for_conversation/3)
|
||||||
|
description("The comments for the conversation")
|
||||||
|
end
|
||||||
|
|
||||||
|
field(:participants, list_of(:person),
|
||||||
|
# resolve: dataloader(Actors),
|
||||||
|
description: "The list of participants to the conversation"
|
||||||
|
)
|
||||||
|
|
||||||
|
field(:event, :event, description: "The event this conversation is associated to")
|
||||||
|
|
||||||
|
field(:actor, :person,
|
||||||
|
# resolve: dataloader(Actors),
|
||||||
|
description: "The actor concerned by the conversation"
|
||||||
|
)
|
||||||
|
|
||||||
|
field(:unread, :boolean, description: "Whether this conversation is unread")
|
||||||
|
|
||||||
|
field(:inserted_at, :datetime, description: "When was this conversation's created")
|
||||||
|
field(:updated_at, :datetime, description: "When was this conversation's updated")
|
||||||
|
end
|
||||||
|
|
||||||
|
@desc "A paginated list of conversations"
|
||||||
|
object :paginated_conversation_list do
|
||||||
|
meta(:authorize, :user)
|
||||||
|
field(:elements, list_of(:conversation), description: "A list of conversations")
|
||||||
|
field(:total, :integer, description: "The total number of conversations in the list")
|
||||||
|
end
|
||||||
|
|
||||||
|
object :conversation_queries do
|
||||||
|
@desc "Get a conversation"
|
||||||
|
field :conversation, type: :conversation do
|
||||||
|
arg(:id, :id, description: "The conversation's ID")
|
||||||
|
|
||||||
|
middleware(Rajska.QueryAuthorization,
|
||||||
|
permit: :user,
|
||||||
|
scope: Mobilizon.Conversations.Conversation,
|
||||||
|
rule: :"read:conversations",
|
||||||
|
args: %{id: :id}
|
||||||
|
)
|
||||||
|
|
||||||
|
resolve(&Conversation.get_conversation/3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
object :conversation_mutations do
|
||||||
|
@desc "Post a private message"
|
||||||
|
field :post_private_message, type: :conversation do
|
||||||
|
arg(:text, non_null(:string), description: "The conversation's first comment body")
|
||||||
|
arg(:actor_id, non_null(:id), description: "The profile ID to create the conversation as")
|
||||||
|
arg(:attributed_to_id, :id, description: "The group ID to attribute the conversation to")
|
||||||
|
arg(:conversation_id, :id, description: "The conversation ID to reply to")
|
||||||
|
arg(:language, :string, description: "The comment language", default_value: "und")
|
||||||
|
arg(:mentions, list_of(:string), description: "A list of federated usernames to mention")
|
||||||
|
|
||||||
|
middleware(Rajska.QueryAuthorization,
|
||||||
|
permit: :user,
|
||||||
|
scope: Mobilizon.Conversations.ConversationParticipant,
|
||||||
|
rule: :"write:conversation:create",
|
||||||
|
args: %{actor_id: :actor_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
resolve(&Conversation.create_conversation/3)
|
||||||
|
end
|
||||||
|
|
||||||
|
@desc "Update a conversation"
|
||||||
|
field :update_conversation, type: :conversation do
|
||||||
|
arg(:conversation_id, non_null(:id), description: "The conversation's ID")
|
||||||
|
arg(:read, non_null(:boolean), description: "Whether the conversation is read or not")
|
||||||
|
|
||||||
|
middleware(Rajska.QueryAuthorization,
|
||||||
|
permit: :user,
|
||||||
|
scope: Mobilizon.Conversations.Conversation,
|
||||||
|
rule: :"write:conversation:update",
|
||||||
|
args: %{id: :conversation_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
resolve(&Conversation.update_conversation/3)
|
||||||
|
end
|
||||||
|
|
||||||
|
@desc "Delete a conversation"
|
||||||
|
field :delete_conversation, type: :conversation do
|
||||||
|
arg(:conversation_id, non_null(:id), description: "The conversation's ID")
|
||||||
|
|
||||||
|
middleware(Rajska.QueryAuthorization,
|
||||||
|
permit: :user,
|
||||||
|
scope: Mobilizon.Conversations.Conversation,
|
||||||
|
rule: :"write:conversation:delete",
|
||||||
|
args: %{id: :conversation_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
resolve(&Conversation.delete_conversation/3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
object :conversation_subscriptions do
|
||||||
|
@desc "Notify when a conversation changed"
|
||||||
|
field :conversation_comment_changed, :conversation do
|
||||||
|
arg(:id, non_null(:id), description: "The conversation's ID")
|
||||||
|
|
||||||
|
config(fn args, _ ->
|
||||||
|
{:ok, topic: args.id}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -56,6 +56,8 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
|
||||||
description: "Whether this comment needs to be announced to participants"
|
description: "Whether this comment needs to be announced to participants"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
field(:conversation, :conversation, description: "The conversation this comment is part of")
|
||||||
|
|
||||||
field(:language, :string, description: "The comment language")
|
field(:language, :string, description: "The comment language")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1, dataloader: 2]
|
import Absinthe.Resolution.Helpers, only: [dataloader: 1, dataloader: 2]
|
||||||
|
|
||||||
alias Mobilizon.{Actors, Addresses, Discussions}
|
alias Mobilizon.{Actors, Addresses, Discussions}
|
||||||
alias Mobilizon.GraphQL.Resolvers.{Event, Media, Tag}
|
alias Mobilizon.GraphQL.Resolvers.{Conversation, Event, Media, Tag}
|
||||||
alias Mobilizon.GraphQL.Schema
|
alias Mobilizon.GraphQL.Schema
|
||||||
|
|
||||||
import_types(Schema.AddressType)
|
import_types(Schema.AddressType)
|
||||||
|
@ -113,6 +113,18 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||||
field(:options, :event_options, description: "The event options")
|
field(:options, :event_options, description: "The event options")
|
||||||
field(:metadata, list_of(:event_metadata), description: "A key-value list of metadata")
|
field(:metadata, list_of(:event_metadata), description: "A key-value list of metadata")
|
||||||
field(:language, :string, description: "The event language")
|
field(:language, :string, description: "The event language")
|
||||||
|
|
||||||
|
field(:conversations, :paginated_conversation_list,
|
||||||
|
description: "The list of conversations started on this event"
|
||||||
|
) do
|
||||||
|
arg(:page, :integer,
|
||||||
|
default_value: 1,
|
||||||
|
description: "The page in the paginated conversation list"
|
||||||
|
)
|
||||||
|
|
||||||
|
arg(:limit, :integer, default_value: 10, description: "The limit of conversations per page")
|
||||||
|
resolve(&Conversation.find_conversations_for_event/3)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@desc "The list of visibility options for an event"
|
@desc "The list of visibility options for an event"
|
||||||
|
|
|
@ -159,5 +159,34 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
|
||||||
|
|
||||||
resolve(&Participant.export_event_participants/3)
|
resolve(&Participant.export_event_participants/3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@desc "Send private messages to participants"
|
||||||
|
field :send_event_private_message, :conversation do
|
||||||
|
arg(:event_id, non_null(:id),
|
||||||
|
description: "The ID from the event for which to export participants"
|
||||||
|
)
|
||||||
|
|
||||||
|
arg(:roles, list_of(:participant_role_enum),
|
||||||
|
default_value: [],
|
||||||
|
description: "The participant roles to include"
|
||||||
|
)
|
||||||
|
|
||||||
|
arg(:text, non_null(:string), description: "The private message body")
|
||||||
|
|
||||||
|
arg(:actor_id, non_null(:id),
|
||||||
|
description: "The profile ID to create the private message as"
|
||||||
|
)
|
||||||
|
|
||||||
|
arg(:language, :string, description: "The private message language", default_value: "und")
|
||||||
|
|
||||||
|
middleware(Rajska.QueryAuthorization,
|
||||||
|
permit: :user,
|
||||||
|
scope: Mobilizon.Events.Event,
|
||||||
|
rule: :"write:event:participants:private_message",
|
||||||
|
args: %{id: :event_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
resolve(&Participant.send_private_messages_to_participants/3)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||||
|
|
||||||
alias Mobilizon.Events
|
alias Mobilizon.Events
|
||||||
alias Mobilizon.GraphQL.Resolvers.Application, as: ApplicationResolver
|
alias Mobilizon.GraphQL.Resolvers.Application, as: ApplicationResolver
|
||||||
alias Mobilizon.GraphQL.Resolvers.{Media, User}
|
alias Mobilizon.GraphQL.Resolvers.{Conversation, Media, User}
|
||||||
alias Mobilizon.GraphQL.Resolvers.Users.ActivitySettings
|
alias Mobilizon.GraphQL.Resolvers.Users.ActivitySettings
|
||||||
alias Mobilizon.GraphQL.Schema
|
alias Mobilizon.GraphQL.Schema
|
||||||
|
|
||||||
|
@ -191,6 +191,19 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||||
) do
|
) do
|
||||||
resolve(&ApplicationResolver.get_user_applications/3)
|
resolve(&ApplicationResolver.get_user_applications/3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@desc "The list of conversations this person has"
|
||||||
|
field(:conversations, :paginated_conversation_list,
|
||||||
|
meta: [private: true, rule: :"read:profile:conversations"]
|
||||||
|
) do
|
||||||
|
arg(:page, :integer,
|
||||||
|
default_value: 1,
|
||||||
|
description: "The page in the conversations list"
|
||||||
|
)
|
||||||
|
|
||||||
|
arg(:limit, :integer, default_value: 10, description: "The limit of conversations per page")
|
||||||
|
resolve(&Conversation.list_conversations/3)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@desc "The list of roles an user can have"
|
@desc "The list of roles an user can have"
|
||||||
|
|
|
@ -17,10 +17,24 @@ defmodule Mobilizon.Activities do
|
||||||
very_high: 50
|
very_high: 50
|
||||||
)
|
)
|
||||||
|
|
||||||
@activity_types ["event", "post", "discussion", "resource", "group", "member", "comment"]
|
@activity_types [
|
||||||
|
"event",
|
||||||
|
"post",
|
||||||
|
"conversation",
|
||||||
|
"discussion",
|
||||||
|
"resource",
|
||||||
|
"group",
|
||||||
|
"member",
|
||||||
|
"comment"
|
||||||
|
]
|
||||||
@event_activity_subjects ["event_created", "event_updated", "event_deleted", "comment_posted"]
|
@event_activity_subjects ["event_created", "event_updated", "event_deleted", "comment_posted"]
|
||||||
@participant_activity_subjects ["event_new_participation"]
|
@participant_activity_subjects ["event_new_participation"]
|
||||||
@post_activity_subjects ["post_created", "post_updated", "post_deleted"]
|
@post_activity_subjects ["post_created", "post_updated", "post_deleted"]
|
||||||
|
@conversation_activity_subjects [
|
||||||
|
"conversation_created",
|
||||||
|
"conversation_replied",
|
||||||
|
"conversation_event_announcement"
|
||||||
|
]
|
||||||
@discussion_activity_subjects [
|
@discussion_activity_subjects [
|
||||||
"discussion_created",
|
"discussion_created",
|
||||||
"discussion_replied",
|
"discussion_replied",
|
||||||
|
@ -49,6 +63,7 @@ defmodule Mobilizon.Activities do
|
||||||
@settings_activity_subjects ["group_created", "group_updated"]
|
@settings_activity_subjects ["group_created", "group_updated"]
|
||||||
|
|
||||||
@subjects @event_activity_subjects ++
|
@subjects @event_activity_subjects ++
|
||||||
|
@conversation_activity_subjects ++
|
||||||
@participant_activity_subjects ++
|
@participant_activity_subjects ++
|
||||||
@post_activity_subjects ++
|
@post_activity_subjects ++
|
||||||
@discussion_activity_subjects ++
|
@discussion_activity_subjects ++
|
||||||
|
@ -61,6 +76,7 @@ defmodule Mobilizon.Activities do
|
||||||
"actor",
|
"actor",
|
||||||
"post",
|
"post",
|
||||||
"discussion",
|
"discussion",
|
||||||
|
"conversation",
|
||||||
"resource",
|
"resource",
|
||||||
"member",
|
"member",
|
||||||
"group",
|
"group",
|
||||||
|
|
|
@ -10,6 +10,7 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
alias Mobilizon.{Actors, Addresses, Config, Crypto, Mention, Share}
|
alias Mobilizon.{Actors, Addresses, Config, Crypto, Mention, Share}
|
||||||
alias Mobilizon.Actors.{ActorOpenness, ActorType, ActorVisibility, Follower, Member}
|
alias Mobilizon.Actors.{ActorOpenness, ActorType, ActorVisibility, Follower, Member}
|
||||||
alias Mobilizon.Addresses.Address
|
alias Mobilizon.Addresses.Address
|
||||||
|
alias Mobilizon.Conversations.Conversation
|
||||||
alias Mobilizon.Discussions.Comment
|
alias Mobilizon.Discussions.Comment
|
||||||
alias Mobilizon.Events.{Event, FeedToken, Participant}
|
alias Mobilizon.Events.{Event, FeedToken, Participant}
|
||||||
alias Mobilizon.Medias.File
|
alias Mobilizon.Medias.File
|
||||||
|
@ -196,6 +197,11 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
has_many(:owner_shares, Share, foreign_key: :owner_actor_id)
|
has_many(:owner_shares, Share, foreign_key: :owner_actor_id)
|
||||||
many_to_many(:memberships, __MODULE__, join_through: Member)
|
many_to_many(:memberships, __MODULE__, join_through: Member)
|
||||||
|
|
||||||
|
many_to_many(:conversations, Conversation,
|
||||||
|
join_through: "conversation_participants",
|
||||||
|
join_keys: [conversation_id: :id, participant_id: :id]
|
||||||
|
)
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
57
lib/mobilizon/conversations/conversation.ex
Normal file
57
lib/mobilizon/conversations/conversation.ex
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
defmodule Mobilizon.Conversations.Conversation do
|
||||||
|
@moduledoc """
|
||||||
|
Represents a conversation
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
alias Ecto.Changeset
|
||||||
|
alias Mobilizon.Actors.Actor
|
||||||
|
alias Mobilizon.Conversations.ConversationParticipant
|
||||||
|
alias Mobilizon.Discussions.Comment
|
||||||
|
alias Mobilizon.Events.Event
|
||||||
|
|
||||||
|
@type t :: %__MODULE__{
|
||||||
|
id: String.t(),
|
||||||
|
origin_comment: Comment.t(),
|
||||||
|
last_comment: Comment.t(),
|
||||||
|
participants: list(Actor.t())
|
||||||
|
}
|
||||||
|
|
||||||
|
@required_attrs [:origin_comment_id, :last_comment_id]
|
||||||
|
@optional_attrs [:event_id]
|
||||||
|
@attrs @required_attrs ++ @optional_attrs
|
||||||
|
|
||||||
|
schema "conversations" do
|
||||||
|
belongs_to(:origin_comment, Comment)
|
||||||
|
belongs_to(:last_comment, Comment)
|
||||||
|
belongs_to(:event, Event)
|
||||||
|
has_many(:comments, Comment)
|
||||||
|
|
||||||
|
many_to_many(:participants, Actor,
|
||||||
|
join_through: ConversationParticipant,
|
||||||
|
join_keys: [conversation_id: :id, actor_id: :id],
|
||||||
|
on_replace: :delete
|
||||||
|
)
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
@spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t()
|
||||||
|
def changeset(%__MODULE__{} = conversation, attrs) do
|
||||||
|
conversation
|
||||||
|
|> cast(attrs, @attrs)
|
||||||
|
|> maybe_set_participants(attrs)
|
||||||
|
|> validate_required(@required_attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_set_participants(%Changeset{} = changeset, %{participants: participants})
|
||||||
|
when length(participants) > 0 do
|
||||||
|
put_assoc(changeset, :participants, participants)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_set_participants(%Changeset{} = changeset, _), do: changeset
|
||||||
|
end
|
40
lib/mobilizon/conversations/conversation_participant.ex
Normal file
40
lib/mobilizon/conversations/conversation_participant.ex
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
defmodule Mobilizon.Conversations.ConversationParticipant do
|
||||||
|
@moduledoc """
|
||||||
|
Represents a conversation participant
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
alias Mobilizon.Actors.Actor
|
||||||
|
alias Mobilizon.Conversations.Conversation
|
||||||
|
|
||||||
|
@type t :: %__MODULE__{
|
||||||
|
conversation: Conversation.t(),
|
||||||
|
actor: Actor.t(),
|
||||||
|
unread: boolean()
|
||||||
|
}
|
||||||
|
|
||||||
|
@required_attrs [:actor_id, :conversation_id]
|
||||||
|
@optional_attrs [:unread]
|
||||||
|
@attrs @required_attrs ++ @optional_attrs
|
||||||
|
|
||||||
|
schema "conversation_participants" do
|
||||||
|
belongs_to(:conversation, Conversation)
|
||||||
|
belongs_to(:actor, Actor)
|
||||||
|
field(:unread, :boolean, default: true)
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
@spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t()
|
||||||
|
def changeset(%__MODULE__{} = conversation, attrs) do
|
||||||
|
conversation
|
||||||
|
|> cast(attrs, @attrs)
|
||||||
|
|> validate_required(@required_attrs)
|
||||||
|
|> foreign_key_constraint(:conversation_id)
|
||||||
|
|> foreign_key_constraint(:actor_id)
|
||||||
|
end
|
||||||
|
end
|
22
lib/mobilizon/conversations/conversation_view.ex
Normal file
22
lib/mobilizon/conversations/conversation_view.ex
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
defmodule Mobilizon.Conversations.ConversationView do
|
||||||
|
@moduledoc """
|
||||||
|
Represents a conversation view for GraphQL API
|
||||||
|
"""
|
||||||
|
|
||||||
|
defstruct [
|
||||||
|
:id,
|
||||||
|
:conversation_participant_id,
|
||||||
|
:origin_comment,
|
||||||
|
:origin_comment_id,
|
||||||
|
:last_comment,
|
||||||
|
:last_comment_id,
|
||||||
|
:event,
|
||||||
|
:event_id,
|
||||||
|
:actor,
|
||||||
|
:actor_id,
|
||||||
|
:unread,
|
||||||
|
:inserted_at,
|
||||||
|
:updated_at,
|
||||||
|
:participants
|
||||||
|
]
|
||||||
|
end
|
344
lib/mobilizon/conversations/conversations.ex
Normal file
344
lib/mobilizon/conversations/conversations.ex
Normal file
|
@ -0,0 +1,344 @@
|
||||||
|
defmodule Mobilizon.Conversations do
|
||||||
|
@moduledoc """
|
||||||
|
The conversations context
|
||||||
|
"""
|
||||||
|
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
alias Ecto.Changeset
|
||||||
|
alias Ecto.Multi
|
||||||
|
alias Mobilizon.Actors.{Actor, Member}
|
||||||
|
alias Mobilizon.Conversations.{Conversation, ConversationParticipant}
|
||||||
|
alias Mobilizon.Discussions.Comment
|
||||||
|
alias Mobilizon.Events.Event
|
||||||
|
alias Mobilizon.Storage.{Page, Repo}
|
||||||
|
|
||||||
|
@conversation_preloads [
|
||||||
|
:origin_comment,
|
||||||
|
:last_comment,
|
||||||
|
:event,
|
||||||
|
:participants
|
||||||
|
]
|
||||||
|
|
||||||
|
@comment_preloads [
|
||||||
|
:actor,
|
||||||
|
:event,
|
||||||
|
:attributed_to,
|
||||||
|
:in_reply_to_comment,
|
||||||
|
:origin_comment,
|
||||||
|
:replies,
|
||||||
|
:tags,
|
||||||
|
:mentions,
|
||||||
|
:media
|
||||||
|
]
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Get a conversation by it's ID
|
||||||
|
"""
|
||||||
|
@spec get_conversation(String.t() | integer()) :: Conversation.t() | nil
|
||||||
|
def get_conversation(conversation_id) do
|
||||||
|
Conversation
|
||||||
|
|> Repo.get(conversation_id)
|
||||||
|
|> Repo.preload(@conversation_preloads)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Get a conversation by it's ID
|
||||||
|
"""
|
||||||
|
@spec get_conversation_participant(String.t() | integer()) :: Conversation.t() | nil
|
||||||
|
def get_conversation_participant(conversation_participant_id) do
|
||||||
|
preload_conversation_participant_details()
|
||||||
|
|> where([cp], cp.id == ^conversation_participant_id)
|
||||||
|
|> Repo.one()
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_participant_by_conversation_and_actor(conversation_id, actor_id) do
|
||||||
|
preload_conversation_participant_details()
|
||||||
|
|> where([cp], cp.conversation_id == ^conversation_id and cp.actor_id == ^actor_id)
|
||||||
|
|> Repo.one()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp preload_conversation_participant_details do
|
||||||
|
ConversationParticipant
|
||||||
|
|> join(:inner, [cp], c in Conversation, on: cp.conversation_id == c.id)
|
||||||
|
|> join(:left, [_cp, c], e in Event, on: c.event_id == e.id)
|
||||||
|
|> join(:inner, [cp], a in Actor, on: cp.actor_id == a.id)
|
||||||
|
|> join(:inner, [_cp, c], lc in Comment, on: c.last_comment_id == lc.id)
|
||||||
|
|> join(:inner, [_cp, c], oc in Comment, on: c.origin_comment_id == oc.id)
|
||||||
|
|> join(:inner, [_cp, c], p in ConversationParticipant, on: c.id == p.conversation_id)
|
||||||
|
|> join(:inner, [_cp, _c, _e, _a, _lc, _oc, p], ap in Actor, on: p.actor_id == ap.id)
|
||||||
|
|> preload([_cp, c, e, a, lc, oc, p, ap],
|
||||||
|
actor: a,
|
||||||
|
conversation:
|
||||||
|
{c, event: e, last_comment: lc, origin_comment: oc, participants: {p, actor: ap}}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Get a paginated list of conversations for an actor
|
||||||
|
"""
|
||||||
|
@spec find_conversations_for_actor(Actor.t(), integer | nil, integer | nil) ::
|
||||||
|
Page.t(Conversation.t())
|
||||||
|
def find_conversations_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
||||||
|
Conversation
|
||||||
|
|> where([c], c.actor_id == ^actor_id)
|
||||||
|
|> preload(^@conversation_preloads)
|
||||||
|
|> order_by(desc: :updated_at)
|
||||||
|
|> Page.build_page(page, limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec find_conversations_for_event(
|
||||||
|
String.t() | integer,
|
||||||
|
String.t() | integer,
|
||||||
|
integer | nil,
|
||||||
|
integer | nil
|
||||||
|
) :: Page.t(ConversationParticipant.t())
|
||||||
|
def find_conversations_for_event(event_id, actor_id, page \\ nil, limit \\ nil) do
|
||||||
|
ConversationParticipant
|
||||||
|
|> join(:inner, [cp], c in Conversation, on: cp.conversation_id == c.id)
|
||||||
|
|> join(:left, [_cp, c], e in Event, on: c.event_id == e.id)
|
||||||
|
|> join(:inner, [cp], a in Actor, on: cp.actor_id == a.id)
|
||||||
|
|> join(:inner, [_cp, c], lc in Comment, on: c.last_comment_id == lc.id)
|
||||||
|
|> join(:inner, [_cp, c], oc in Comment, on: c.origin_comment_id == oc.id)
|
||||||
|
|> join(:inner, [_cp, c], p in ConversationParticipant, on: c.id == p.conversation_id)
|
||||||
|
|> join(:inner, [_cp, _c, _e, _a, _lc, _oc, p], ap in Actor, on: p.actor_id == ap.id)
|
||||||
|
|> where([_cp, c], c.event_id == ^event_id)
|
||||||
|
|> where([cp], cp.actor_id == ^actor_id)
|
||||||
|
|> preload([_cp, c, e, a, lc, oc, p, ap],
|
||||||
|
actor: a,
|
||||||
|
conversation:
|
||||||
|
{c, event: e, last_comment: lc, origin_comment: oc, participants: {p, actor: ap}}
|
||||||
|
)
|
||||||
|
|> Page.build_page(page, limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec list_conversation_participants_for_actor(
|
||||||
|
integer | String.t(),
|
||||||
|
integer | nil,
|
||||||
|
integer | nil
|
||||||
|
) ::
|
||||||
|
Page.t(ConversationParticipant.t())
|
||||||
|
def list_conversation_participants_for_actor(actor_id, page \\ nil, limit \\ nil) do
|
||||||
|
subquery =
|
||||||
|
ConversationParticipant
|
||||||
|
|> distinct([cp], cp.conversation_id)
|
||||||
|
|> join(:left, [cp], m in Member, on: cp.actor_id == m.parent_id)
|
||||||
|
|> where([cp], cp.actor_id == ^actor_id)
|
||||||
|
|> or_where(
|
||||||
|
[_cp, m],
|
||||||
|
m.actor_id == ^actor_id and m.role in [:creator, :administrator, :moderator]
|
||||||
|
)
|
||||||
|
|
||||||
|
subquery
|
||||||
|
|> subquery()
|
||||||
|
|> order_by([cp], desc: cp.unread, desc: cp.updated_at)
|
||||||
|
|> preload([:actor, conversation: [:last_comment, :participants]])
|
||||||
|
|> Page.build_page(page, limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec list_conversation_participants_for_user(
|
||||||
|
integer | String.t(),
|
||||||
|
integer | nil,
|
||||||
|
integer | nil
|
||||||
|
) ::
|
||||||
|
Page.t(ConversationParticipant.t())
|
||||||
|
def list_conversation_participants_for_user(user_id, page \\ nil, limit \\ nil) do
|
||||||
|
ConversationParticipant
|
||||||
|
|> join(:inner, [cp], a in Actor, on: cp.actor_id == a.id)
|
||||||
|
|> where([_cp, a], a.user_id == ^user_id)
|
||||||
|
|> preload([:actor, conversation: [:last_comment, :participants]])
|
||||||
|
|> Page.build_page(page, limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec list_conversation_participants_for_conversation(integer | String.t()) ::
|
||||||
|
list(ConversationParticipant.t())
|
||||||
|
def list_conversation_participants_for_conversation(conversation_id) do
|
||||||
|
ConversationParticipant
|
||||||
|
|> where([cp], cp.conversation_id == ^conversation_id)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec count_unread_conversation_participants_for_person(integer | String.t()) ::
|
||||||
|
non_neg_integer()
|
||||||
|
def count_unread_conversation_participants_for_person(actor_id) do
|
||||||
|
ConversationParticipant
|
||||||
|
|> where([cp], cp.actor_id == ^actor_id and cp.unread == true)
|
||||||
|
|> Repo.aggregate(:count)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a conversation.
|
||||||
|
"""
|
||||||
|
@spec create_conversation(map()) ::
|
||||||
|
{:ok, Conversation.t()} | {:error, atom(), Changeset.t(), map()}
|
||||||
|
def create_conversation(attrs) do
|
||||||
|
with {:ok, %{comment: %Comment{} = _comment, conversation: %Conversation{} = conversation}} <-
|
||||||
|
Multi.new()
|
||||||
|
|> Multi.insert(
|
||||||
|
:comment,
|
||||||
|
Comment.changeset(
|
||||||
|
%Comment{},
|
||||||
|
Map.merge(attrs, %{
|
||||||
|
actor_id: attrs.actor_id,
|
||||||
|
attributed_to_id: attrs.actor_id,
|
||||||
|
visibility: :private
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|> Multi.insert(:conversation, fn %{
|
||||||
|
comment: %Comment{
|
||||||
|
id: comment_id,
|
||||||
|
origin_comment_id: origin_comment_id
|
||||||
|
}
|
||||||
|
} ->
|
||||||
|
Conversation.changeset(
|
||||||
|
%Conversation{},
|
||||||
|
Map.merge(attrs, %{
|
||||||
|
last_comment_id: comment_id,
|
||||||
|
origin_comment_id: origin_comment_id || comment_id,
|
||||||
|
participants: attrs.participants
|
||||||
|
})
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|> Multi.update(:update_comment, fn %{
|
||||||
|
comment: %Comment{} = comment,
|
||||||
|
conversation: %Conversation{id: conversation_id}
|
||||||
|
} ->
|
||||||
|
Comment.changeset(
|
||||||
|
comment,
|
||||||
|
%{conversation_id: conversation_id}
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|> Multi.update_all(
|
||||||
|
:conversation_participants,
|
||||||
|
fn %{
|
||||||
|
conversation: %Conversation{
|
||||||
|
id: conversation_id
|
||||||
|
}
|
||||||
|
} ->
|
||||||
|
ConversationParticipant
|
||||||
|
|> where(
|
||||||
|
[cp],
|
||||||
|
cp.conversation_id == ^conversation_id and cp.actor_id == ^attrs.actor_id
|
||||||
|
)
|
||||||
|
|> update([cp], set: [unread: false, updated_at: ^NaiveDateTime.utc_now()])
|
||||||
|
end,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|> Repo.transaction(),
|
||||||
|
%Conversation{} = conversation <- Repo.preload(conversation, @conversation_preloads) do
|
||||||
|
{:ok, conversation}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Create a response to a conversation
|
||||||
|
"""
|
||||||
|
@spec reply_to_conversation(Conversation.t(), map()) ::
|
||||||
|
{:ok, Conversation.t()} | {:error, atom(), Ecto.Changeset.t(), map()}
|
||||||
|
def reply_to_conversation(%Conversation{id: conversation_id} = conversation, attrs \\ %{}) do
|
||||||
|
attrs =
|
||||||
|
Map.merge(attrs, %{
|
||||||
|
conversation_id: conversation_id,
|
||||||
|
actor_id: Map.get(attrs, :creator_id, Map.get(attrs, :actor_id)),
|
||||||
|
origin_comment_id: conversation.origin_comment_id,
|
||||||
|
in_reply_to_comment_id: conversation.last_comment_id,
|
||||||
|
visibility: :private
|
||||||
|
})
|
||||||
|
|
||||||
|
changeset =
|
||||||
|
Comment.changeset(
|
||||||
|
%Comment{},
|
||||||
|
attrs
|
||||||
|
)
|
||||||
|
|
||||||
|
with {:ok, %{comment: %Comment{} = comment, conversation: %Conversation{} = conversation}} <-
|
||||||
|
Multi.new()
|
||||||
|
|> Multi.insert(
|
||||||
|
:comment,
|
||||||
|
changeset
|
||||||
|
)
|
||||||
|
|> Multi.update(:conversation, fn %{comment: %Comment{id: comment_id}} ->
|
||||||
|
Conversation.changeset(
|
||||||
|
conversation,
|
||||||
|
%{last_comment_id: comment_id}
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|> Multi.update_all(
|
||||||
|
:conversation_participants,
|
||||||
|
fn %{
|
||||||
|
conversation: %Conversation{
|
||||||
|
id: conversation_id
|
||||||
|
}
|
||||||
|
} ->
|
||||||
|
ConversationParticipant
|
||||||
|
|> where(
|
||||||
|
[cp],
|
||||||
|
cp.conversation_id == ^conversation_id and cp.actor_id != ^attrs.actor_id
|
||||||
|
)
|
||||||
|
|> update([cp], set: [unread: true, updated_at: ^NaiveDateTime.utc_now()])
|
||||||
|
end,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|> Multi.update_all(
|
||||||
|
:conversation_participants_author,
|
||||||
|
fn %{
|
||||||
|
conversation: %Conversation{
|
||||||
|
id: conversation_id
|
||||||
|
}
|
||||||
|
} ->
|
||||||
|
ConversationParticipant
|
||||||
|
|> where(
|
||||||
|
[cp],
|
||||||
|
cp.conversation_id == ^conversation_id and cp.actor_id == ^attrs.actor_id
|
||||||
|
)
|
||||||
|
|> update([cp], set: [unread: false, updated_at: ^NaiveDateTime.utc_now()])
|
||||||
|
end,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|> Repo.transaction(),
|
||||||
|
# Conversation is not updated
|
||||||
|
%Comment{} = comment <- Repo.preload(comment, @comment_preloads) do
|
||||||
|
{:ok, %Conversation{conversation | last_comment: comment}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Update a conversation.
|
||||||
|
"""
|
||||||
|
@spec update_conversation(Conversation.t(), map()) ::
|
||||||
|
{:ok, Conversation.t()} | {:error, Changeset.t()}
|
||||||
|
def update_conversation(%Conversation{} = conversation, attrs \\ %{}) do
|
||||||
|
conversation
|
||||||
|
|> Conversation.changeset(attrs)
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Delete a conversation.
|
||||||
|
"""
|
||||||
|
@spec delete_conversation(Conversation.t()) ::
|
||||||
|
{:ok, %{comments: {integer() | nil, any()}}} | {:error, :comments, Changeset.t(), map()}
|
||||||
|
def delete_conversation(%Conversation{id: conversation_id}) do
|
||||||
|
Multi.new()
|
||||||
|
|> Multi.delete_all(:comments, fn _ ->
|
||||||
|
where(Comment, [c], c.conversation_id == ^conversation_id)
|
||||||
|
end)
|
||||||
|
# |> Multi.delete(:conversation, conversation)
|
||||||
|
|> Repo.transaction()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Update a conversation participant. Only their read status for now
|
||||||
|
"""
|
||||||
|
@spec update_conversation_participant(ConversationParticipant.t(), map()) ::
|
||||||
|
{:ok, ConversationParticipant.t()} | {:error, Changeset.t()}
|
||||||
|
def update_conversation_participant(
|
||||||
|
%ConversationParticipant{} = conversation_participant,
|
||||||
|
attrs \\ %{}
|
||||||
|
) do
|
||||||
|
conversation_participant
|
||||||
|
|> ConversationParticipant.changeset(attrs)
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,6 +9,7 @@ defmodule Mobilizon.Discussions.Comment do
|
||||||
import Mobilizon.Storage.Ecto, only: [maybe_add_published_at: 1]
|
import Mobilizon.Storage.Ecto, only: [maybe_add_published_at: 1]
|
||||||
|
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
|
alias Mobilizon.Conversations.Conversation
|
||||||
alias Mobilizon.Discussions.{Comment, CommentVisibility, Discussion}
|
alias Mobilizon.Discussions.{Comment, CommentVisibility, Discussion}
|
||||||
alias Mobilizon.Events.{Event, Tag}
|
alias Mobilizon.Events.{Event, Tag}
|
||||||
alias Mobilizon.Medias.Media
|
alias Mobilizon.Medias.Media
|
||||||
|
@ -49,7 +50,9 @@ defmodule Mobilizon.Discussions.Comment do
|
||||||
:local,
|
:local,
|
||||||
:is_announcement,
|
:is_announcement,
|
||||||
:discussion_id,
|
:discussion_id,
|
||||||
:language
|
:conversation_id,
|
||||||
|
:language,
|
||||||
|
:visibility
|
||||||
]
|
]
|
||||||
@attrs @required_attrs ++ @optional_attrs
|
@attrs @required_attrs ++ @optional_attrs
|
||||||
|
|
||||||
|
@ -71,16 +74,17 @@ defmodule Mobilizon.Discussions.Comment do
|
||||||
belongs_to(:in_reply_to_comment, Comment, foreign_key: :in_reply_to_comment_id)
|
belongs_to(:in_reply_to_comment, Comment, foreign_key: :in_reply_to_comment_id)
|
||||||
belongs_to(:origin_comment, Comment, foreign_key: :origin_comment_id)
|
belongs_to(:origin_comment, Comment, foreign_key: :origin_comment_id)
|
||||||
belongs_to(:discussion, Discussion, type: :binary_id)
|
belongs_to(:discussion, Discussion, type: :binary_id)
|
||||||
|
belongs_to(:conversation, Conversation)
|
||||||
has_many(:replies, Comment, foreign_key: :origin_comment_id)
|
has_many(:replies, Comment, foreign_key: :origin_comment_id)
|
||||||
many_to_many(:tags, Tag, join_through: "comments_tags", on_replace: :delete)
|
many_to_many(:tags, Tag, join_through: "comments_tags", on_replace: :delete)
|
||||||
has_many(:mentions, Mention)
|
has_many(:mentions, Mention, on_replace: :delete)
|
||||||
many_to_many(:media, Media, join_through: "comments_medias", on_replace: :delete)
|
many_to_many(:media, Media, join_through: "comments_medias", on_replace: :delete)
|
||||||
|
|
||||||
timestamps(type: :utc_datetime)
|
timestamps(type: :utc_datetime)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the id of the first comment in the discussion.
|
Returns the id of the first comment in the discussion or conversation.
|
||||||
"""
|
"""
|
||||||
@spec get_thread_id(t) :: integer
|
@spec get_thread_id(t) :: integer
|
||||||
def get_thread_id(%__MODULE__{id: id, origin_comment_id: origin_comment_id}) do
|
def get_thread_id(%__MODULE__{id: id, origin_comment_id: origin_comment_id}) do
|
||||||
|
@ -181,7 +185,7 @@ defmodule Mobilizon.Discussions.Comment do
|
||||||
Tag.changeset(%Tag{}, tag)
|
Tag.changeset(%Tag{}, tag)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp process_mention(tag) do
|
defp process_mention(mention) do
|
||||||
Mention.changeset(%Mention{}, tag)
|
Mention.changeset(%Mention{}, mention)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -42,9 +42,9 @@ defmodule Mobilizon.Discussions do
|
||||||
:origin_comment,
|
:origin_comment,
|
||||||
:replies,
|
:replies,
|
||||||
:tags,
|
:tags,
|
||||||
:mentions,
|
|
||||||
:discussion,
|
:discussion,
|
||||||
:media
|
:media,
|
||||||
|
mentions: [:actor]
|
||||||
]
|
]
|
||||||
|
|
||||||
@discussion_preloads [
|
@discussion_preloads [
|
||||||
|
@ -76,6 +76,7 @@ defmodule Mobilizon.Discussions do
|
||||||
Comment
|
Comment
|
||||||
|> join(:left, [c], r in Comment, on: r.origin_comment_id == c.id)
|
|> join(:left, [c], r in Comment, on: r.origin_comment_id == c.id)
|
||||||
|> where([c, _], is_nil(c.in_reply_to_comment_id))
|
|> where([c, _], is_nil(c.in_reply_to_comment_id))
|
||||||
|
|> where([c], c.visibility in ^@public_visibility)
|
||||||
# TODO: This was added because we don't want to count deleted comments in total_replies.
|
# TODO: This was added because we don't want to count deleted comments in total_replies.
|
||||||
# However, it also excludes all top-level comments with deleted replies from being selected
|
# However, it also excludes all top-level comments with deleted replies from being selected
|
||||||
# |> where([_, r], is_nil(r.deleted_at))
|
# |> where([_, r], is_nil(r.deleted_at))
|
||||||
|
@ -197,9 +198,13 @@ defmodule Mobilizon.Discussions do
|
||||||
"""
|
"""
|
||||||
@spec update_comment(Comment.t(), map) :: {:ok, Comment.t()} | {:error, Changeset.t()}
|
@spec update_comment(Comment.t(), map) :: {:ok, Comment.t()} | {:error, Changeset.t()}
|
||||||
def update_comment(%Comment{} = comment, attrs) do
|
def update_comment(%Comment{} = comment, attrs) do
|
||||||
comment
|
with {:ok, %Comment{} = comment} <-
|
||||||
|> Comment.update_changeset(attrs)
|
comment
|
||||||
|> Repo.update()
|
|> Comment.update_changeset(attrs)
|
||||||
|
|> Repo.update(),
|
||||||
|
%Comment{} = comment <- Repo.preload(comment, @comment_preloads) do
|
||||||
|
{:ok, comment}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -272,6 +277,19 @@ defmodule Mobilizon.Discussions do
|
||||||
|> Page.build_page(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Get all the comments contained into a discussion
|
||||||
|
"""
|
||||||
|
@spec get_comments_in_reply_to_comment_id(integer, integer | nil, integer | nil) ::
|
||||||
|
Page.t(Comment.t())
|
||||||
|
def get_comments_in_reply_to_comment_id(origin_comment_id, page \\ nil, limit \\ nil) do
|
||||||
|
Comment
|
||||||
|
|> where([c], c.id == ^origin_comment_id)
|
||||||
|
|> or_where([c], c.origin_comment_id == ^origin_comment_id)
|
||||||
|
|> order_by(asc: :inserted_at)
|
||||||
|
|> Page.build_page(page, limit)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Counts local comments under events
|
Counts local comments under events
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -13,6 +13,7 @@ defmodule Mobilizon.Events.Event do
|
||||||
alias Mobilizon.{Addresses, Events, Medias, Mention}
|
alias Mobilizon.{Addresses, Events, Medias, Mention}
|
||||||
alias Mobilizon.Addresses.Address
|
alias Mobilizon.Addresses.Address
|
||||||
|
|
||||||
|
alias Mobilizon.Conversations.Conversation
|
||||||
alias Mobilizon.Discussions.Comment
|
alias Mobilizon.Discussions.Comment
|
||||||
|
|
||||||
alias Mobilizon.Events.{
|
alias Mobilizon.Events.{
|
||||||
|
@ -126,6 +127,7 @@ defmodule Mobilizon.Events.Event do
|
||||||
has_many(:sessions, Session)
|
has_many(:sessions, Session)
|
||||||
has_many(:mentions, Mention)
|
has_many(:mentions, Mention)
|
||||||
has_many(:comments, Comment)
|
has_many(:comments, Comment)
|
||||||
|
has_many(:conversations, Conversation)
|
||||||
many_to_many(:contacts, Actor, join_through: "event_contacts", on_replace: :delete)
|
many_to_many(:contacts, Actor, join_through: "event_contacts", on_replace: :delete)
|
||||||
many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete)
|
many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete)
|
||||||
many_to_many(:participants, Actor, join_through: Participant)
|
many_to_many(:participants, Actor, join_through: Participant)
|
||||||
|
|
|
@ -871,6 +871,21 @@ defmodule Mobilizon.Events do
|
||||||
|> Page.build_page(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the whole list of participants for an event.
|
||||||
|
Default behaviour is to not return :not_approved or :not_confirmed participants
|
||||||
|
"""
|
||||||
|
@spec list_all_participants_for_event(String.t(), list(atom())) :: list(Participant.t())
|
||||||
|
def list_all_participants_for_event(
|
||||||
|
id,
|
||||||
|
roles \\ []
|
||||||
|
) do
|
||||||
|
id
|
||||||
|
|> participants_for_event_query(roles)
|
||||||
|
|> preload([:actor, :event])
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
@spec list_actors_participants_for_event(String.t()) :: [Actor.t()]
|
@spec list_actors_participants_for_event(String.t()) :: [Actor.t()]
|
||||||
def list_actors_participants_for_event(id) do
|
def list_actors_participants_for_event(id) do
|
||||||
id
|
id
|
||||||
|
|
|
@ -32,8 +32,8 @@ defmodule Mobilizon.Mention do
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t()
|
@spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t()
|
||||||
def changeset(event, attrs) do
|
def changeset(mention, attrs) do
|
||||||
event
|
mention
|
||||||
|> cast(attrs, @attrs)
|
|> cast(attrs, @attrs)
|
||||||
# TODO: Enforce having either event_id or comment_id
|
# TODO: Enforce having either event_id or comment_id
|
||||||
|> validate_required(@required_attrs)
|
|> validate_required(@required_attrs)
|
||||||
|
|
|
@ -21,7 +21,14 @@ defmodule Mobilizon.Reports do
|
||||||
def get_report(id) do
|
def get_report(id) do
|
||||||
Report
|
Report
|
||||||
|> Repo.get(id)
|
|> Repo.get(id)
|
||||||
|> Repo.preload([:reported, :reporter, :manager, :events, :comments, :notes])
|
|> Repo.preload([
|
||||||
|
:reported,
|
||||||
|
:reporter,
|
||||||
|
:manager,
|
||||||
|
:events,
|
||||||
|
:notes,
|
||||||
|
comments: [conversation: [:participants]]
|
||||||
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
95
lib/service/activity/conversation.ex
Normal file
95
lib/service/activity/conversation.ex
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
defmodule Mobilizon.Service.Activity.Conversation do
|
||||||
|
@moduledoc """
|
||||||
|
Insert a conversation activity
|
||||||
|
"""
|
||||||
|
alias Mobilizon.Conversations
|
||||||
|
alias Mobilizon.Conversations.{Conversation, ConversationParticipant}
|
||||||
|
alias Mobilizon.Discussions.Comment
|
||||||
|
alias Mobilizon.Events.Event
|
||||||
|
alias Mobilizon.Service.Activity
|
||||||
|
alias Mobilizon.Service.Workers.LegacyNotifierBuilder
|
||||||
|
|
||||||
|
@behaviour Activity
|
||||||
|
|
||||||
|
@impl Activity
|
||||||
|
def insert_activity(conversation, options \\ [])
|
||||||
|
|
||||||
|
def insert_activity(
|
||||||
|
%Conversation{} = conversation,
|
||||||
|
options
|
||||||
|
) do
|
||||||
|
subject = Keyword.fetch!(options, :subject)
|
||||||
|
|
||||||
|
send_participant_notifications(subject, conversation, conversation.last_comment, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_activity(_, _), do: {:ok, nil}
|
||||||
|
|
||||||
|
@impl Activity
|
||||||
|
def get_object(conversation_id) do
|
||||||
|
Conversations.get_conversation(conversation_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# An actor is mentionned
|
||||||
|
@spec send_participant_notifications(String.t(), Discussion.t(), Comment.t(), Keyword.t()) ::
|
||||||
|
{:ok, Oban.Job.t()} | {:ok, :skipped}
|
||||||
|
defp send_participant_notifications(
|
||||||
|
subject,
|
||||||
|
%Conversation{
|
||||||
|
id: conversation_id
|
||||||
|
} = conversation,
|
||||||
|
%Comment{actor_id: actor_id},
|
||||||
|
_options
|
||||||
|
)
|
||||||
|
when subject in [
|
||||||
|
"conversation_created",
|
||||||
|
"conversation_replied",
|
||||||
|
"conversation_event_announcement"
|
||||||
|
] do
|
||||||
|
# We need to send each notification individually as the conversation URL varies for each participant
|
||||||
|
|
||||||
|
conversation_id
|
||||||
|
|> Conversations.list_conversation_participants_for_conversation()
|
||||||
|
|> Enum.each(fn %ConversationParticipant{id: conversation_participant_id} =
|
||||||
|
conversation_participant ->
|
||||||
|
LegacyNotifierBuilder.enqueue(
|
||||||
|
:legacy_notify,
|
||||||
|
%{
|
||||||
|
"subject" => subject,
|
||||||
|
"subject_params" =>
|
||||||
|
Map.merge(
|
||||||
|
%{
|
||||||
|
conversation_id: conversation_id,
|
||||||
|
conversation_participant_id: conversation_participant_id
|
||||||
|
},
|
||||||
|
event_subject_params(conversation)
|
||||||
|
),
|
||||||
|
"type" => :conversation,
|
||||||
|
"object_type" => :conversation,
|
||||||
|
"author_id" => actor_id,
|
||||||
|
"object_id" => to_string(conversation_id),
|
||||||
|
"participant" => Map.take(conversation_participant, [:id, :actor_id])
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
{:ok, :enqueued}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp send_participant_notifications(_, _, _, _), do: {:ok, :skipped}
|
||||||
|
|
||||||
|
defp event_subject_params(%Conversation{
|
||||||
|
event: %Event{
|
||||||
|
id: conversation_event_id,
|
||||||
|
title: conversation_event_title,
|
||||||
|
uuid: conversation_event_uuid
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
do: %{
|
||||||
|
conversation_event_id: conversation_event_id,
|
||||||
|
conversation_event_title: conversation_event_title,
|
||||||
|
conversation_event_uuid: conversation_event_uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
defp event_subject_params(_), do: %{}
|
||||||
|
end
|
73
lib/service/activity/renderer/conversation.ex
Normal file
73
lib/service/activity/renderer/conversation.ex
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
defmodule Mobilizon.Service.Activity.Renderer.Conversation do
|
||||||
|
@moduledoc """
|
||||||
|
Render a conversation activity
|
||||||
|
"""
|
||||||
|
alias Mobilizon.Activities.Activity
|
||||||
|
alias Mobilizon.Actors.Actor
|
||||||
|
alias Mobilizon.Service.Activity.Renderer
|
||||||
|
alias Mobilizon.Web.Endpoint
|
||||||
|
alias Mobilizon.Web.Router.Helpers, as: Routes
|
||||||
|
import Mobilizon.Web.Gettext, only: [dgettext: 3]
|
||||||
|
|
||||||
|
@behaviour Renderer
|
||||||
|
|
||||||
|
@impl Renderer
|
||||||
|
def render(%Activity{} = activity, options) do
|
||||||
|
locale = Keyword.get(options, :locale, "en")
|
||||||
|
Gettext.put_locale(locale)
|
||||||
|
profile = profile(activity)
|
||||||
|
|
||||||
|
case activity.subject do
|
||||||
|
:conversation_created ->
|
||||||
|
%{
|
||||||
|
body:
|
||||||
|
dgettext(
|
||||||
|
"activity",
|
||||||
|
"%{profile} sent you a message",
|
||||||
|
%{
|
||||||
|
profile: profile
|
||||||
|
}
|
||||||
|
),
|
||||||
|
url: conversation_url(activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
:conversation_replied ->
|
||||||
|
%{
|
||||||
|
body:
|
||||||
|
dgettext(
|
||||||
|
"activity",
|
||||||
|
"%{profile} replied to your message",
|
||||||
|
%{
|
||||||
|
profile: profile
|
||||||
|
}
|
||||||
|
),
|
||||||
|
url: conversation_url(activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
:conversation_event_announcement ->
|
||||||
|
%{
|
||||||
|
body:
|
||||||
|
dgettext(
|
||||||
|
"activity",
|
||||||
|
"%{profile} sent a private message about event %{event}",
|
||||||
|
%{
|
||||||
|
profile: profile,
|
||||||
|
event: event_title(activity)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
url: conversation_url(activity)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp conversation_url(activity) do
|
||||||
|
Routes.page_url(
|
||||||
|
Endpoint,
|
||||||
|
:conversation,
|
||||||
|
activity.subject_params["conversation_id"]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp profile(activity), do: Actor.display_name_and_username(activity.author)
|
||||||
|
defp event_title(activity), do: activity.subject_params["conversation_event_title"]
|
||||||
|
end
|
|
@ -51,17 +51,25 @@ defmodule Mobilizon.Service.Activity.Renderer do
|
||||||
res
|
res
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@types_map %{
|
||||||
|
discussion: Discussion,
|
||||||
|
conversation: Conversation,
|
||||||
|
event: Event,
|
||||||
|
group: Group,
|
||||||
|
member: Member,
|
||||||
|
post: Post,
|
||||||
|
resource: Resource,
|
||||||
|
comment: Comment
|
||||||
|
}
|
||||||
|
|
||||||
@spec do_render(Activity.t(), Keyword.t()) :: common_render()
|
@spec do_render(Activity.t(), Keyword.t()) :: common_render()
|
||||||
defp do_render(%Activity{type: type} = activity, options) do
|
defp do_render(%Activity{type: type} = activity, options) do
|
||||||
case type do
|
case Map.get(@types_map, type) do
|
||||||
:discussion -> Discussion.render(activity, options)
|
nil ->
|
||||||
:event -> Event.render(activity, options)
|
nil
|
||||||
:group -> Group.render(activity, options)
|
|
||||||
:member -> Member.render(activity, options)
|
mod ->
|
||||||
:post -> Post.render(activity, options)
|
mod.render(activity, options)
|
||||||
:resource -> Resource.render(activity, options)
|
|
||||||
:comment -> Comment.render(activity, options)
|
|
||||||
_ -> nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,8 @@ defmodule Mobilizon.Service.Formatter.HTML do
|
||||||
|
|
||||||
def filter_tags(html), do: Sanitizer.scrub(html, DefaultScrubbler)
|
def filter_tags(html), do: Sanitizer.scrub(html, DefaultScrubbler)
|
||||||
|
|
||||||
|
defdelegate basic_html(html), to: FastSanitize
|
||||||
|
|
||||||
@spec strip_tags(String.t()) :: String.t() | no_return()
|
@spec strip_tags(String.t()) :: String.t() | no_return()
|
||||||
def strip_tags(html) do
|
def strip_tags(html) do
|
||||||
case FastSanitize.strip_tags(html) do
|
case FastSanitize.strip_tags(html) do
|
||||||
|
@ -39,5 +41,17 @@ defmodule Mobilizon.Service.Formatter.HTML do
|
||||||
|
|
||||||
def strip_tags_and_insert_spaces(html), do: html
|
def strip_tags_and_insert_spaces(html), do: html
|
||||||
|
|
||||||
|
@spec html_to_text(String.t()) :: String.t()
|
||||||
|
def html_to_text(html) do
|
||||||
|
html
|
||||||
|
|> String.replace(~r/<li>/, "\\g{1}- ", global: true)
|
||||||
|
|> String.replace(
|
||||||
|
~r/<\/?\s?br>|<\/\s?p>|<\/\s?li>|<\/\s?div>|<\/\s?h.>/,
|
||||||
|
"\\g{1}\n\r",
|
||||||
|
global: true
|
||||||
|
)
|
||||||
|
|> strip_tags()
|
||||||
|
end
|
||||||
|
|
||||||
def filter_tags_for_oembed(html), do: Sanitizer.scrub(html, OEmbed)
|
def filter_tags_for_oembed(html), do: Sanitizer.scrub(html, OEmbed)
|
||||||
end
|
end
|
||||||
|
|
36
lib/service/formatter/text.ex
Normal file
36
lib/service/formatter/text.ex
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
defmodule Mobilizon.Service.Formatter.Text do
|
||||||
|
@moduledoc """
|
||||||
|
Helps to format text blocks
|
||||||
|
|
||||||
|
Inspired from https://elixirforum.com/t/is-there-are-text-wrapping-library-for-elixir/21733/4
|
||||||
|
Using the Knuth-Plass Line Wrapping Algorithm https://www.students.cs.ubc.ca/~cs-490/2015W2/lectures/Knuth.pdf
|
||||||
|
"""
|
||||||
|
|
||||||
|
def quote_paragraph(string, max_line_length) do
|
||||||
|
paragraph(string, max_line_length, "> ")
|
||||||
|
end
|
||||||
|
|
||||||
|
def paragraph(string, max_line_length, prefix \\ "") do
|
||||||
|
string
|
||||||
|
|> String.split("\n\n", trim: true)
|
||||||
|
|> Enum.map_join("\n#{prefix}\n", &subparagraph(&1, max_line_length, prefix))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp subparagraph(string, max_line_length, prefix) do
|
||||||
|
[word | rest] = String.split(string, ~r/\s+/, trim: true)
|
||||||
|
|
||||||
|
rest
|
||||||
|
|> lines_assemble(max_line_length - String.length(prefix), String.length(word), word, [])
|
||||||
|
|> Enum.map_join("\n", &"#{prefix}#{&1}")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp lines_assemble([], _, _, line, acc), do: [line | acc] |> Enum.reverse()
|
||||||
|
|
||||||
|
defp lines_assemble([word | rest], max, line_length, line, acc) do
|
||||||
|
if line_length + 1 + String.length(word) > max do
|
||||||
|
lines_assemble(rest, max, String.length(word), word, [line | acc])
|
||||||
|
else
|
||||||
|
lines_assemble(rest, max, line_length + 1 + String.length(word), line <> " " <> word, acc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -70,6 +70,9 @@ defmodule Mobilizon.Service.Notifier.Email do
|
||||||
@always_direct_subjects [
|
@always_direct_subjects [
|
||||||
:participation_event_comment,
|
:participation_event_comment,
|
||||||
:event_comment_mention,
|
:event_comment_mention,
|
||||||
|
:conversation_mention,
|
||||||
|
:conversation_created,
|
||||||
|
:conversation_replied,
|
||||||
:discussion_mention,
|
:discussion_mention,
|
||||||
:event_new_comment
|
:event_new_comment
|
||||||
]
|
]
|
||||||
|
@ -175,6 +178,9 @@ defmodule Mobilizon.Service.Notifier.Email do
|
||||||
"member_updated" => false,
|
"member_updated" => false,
|
||||||
"user_email_password_updated" => true,
|
"user_email_password_updated" => true,
|
||||||
"event_comment_mention" => true,
|
"event_comment_mention" => true,
|
||||||
|
"conversation_mention" => true,
|
||||||
|
"conversation_created" => true,
|
||||||
|
"conversation_replied" => true,
|
||||||
"discussion_mention" => true,
|
"discussion_mention" => true,
|
||||||
"event_new_comment" => true
|
"event_new_comment" => true
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,10 @@ defmodule Mobilizon.Service.Notifier.Filter do
|
||||||
defp map_activity_to_activity_setting(%Activity{subject: :event_comment_mention}),
|
defp map_activity_to_activity_setting(%Activity{subject: :event_comment_mention}),
|
||||||
do: "event_comment_mention"
|
do: "event_comment_mention"
|
||||||
|
|
||||||
|
defp map_activity_to_activity_setting(%Activity{subject: subject})
|
||||||
|
when subject in [:conversation_mention, :conversation_created, :conversation_replied],
|
||||||
|
do: to_string(subject)
|
||||||
|
|
||||||
defp map_activity_to_activity_setting(%Activity{subject: :discussion_mention}),
|
defp map_activity_to_activity_setting(%Activity{subject: :discussion_mention}),
|
||||||
do: "discussion_mention"
|
do: "discussion_mention"
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@ defmodule Mobilizon.Service.Notifier.Push do
|
||||||
"member_updated" => false,
|
"member_updated" => false,
|
||||||
"user_email_password_updated" => false,
|
"user_email_password_updated" => false,
|
||||||
"event_comment_mention" => true,
|
"event_comment_mention" => true,
|
||||||
|
"conversation_mention" => true,
|
||||||
"discussion_mention" => false,
|
"discussion_mention" => false,
|
||||||
"event_new_comment" => false
|
"event_new_comment" => false
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilder do
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Events.{Event, Participant}
|
alias Mobilizon.Events.{Event, Participant}
|
||||||
alias Mobilizon.Service.Notifier
|
alias Mobilizon.Service.Notifier
|
||||||
|
require Logger
|
||||||
|
|
||||||
use Mobilizon.Service.Workers.Helper, queue: "activity"
|
use Mobilizon.Service.Workers.Helper, queue: "activity"
|
||||||
|
|
||||||
|
@ -15,14 +16,22 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilder do
|
||||||
def perform(%Job{args: args}) do
|
def perform(%Job{args: args}) do
|
||||||
{"legacy_notify", args} = Map.pop(args, "op")
|
{"legacy_notify", args} = Map.pop(args, "op")
|
||||||
activity = build_activity(args)
|
activity = build_activity(args)
|
||||||
|
Logger.debug("Handling activity #{activity.subject} to notify in LegacyNotifierBuilder")
|
||||||
|
|
||||||
if args["subject"] == "participation_event_comment" do
|
if args["subject"] == "participation_event_comment" do
|
||||||
notify_anonymous_participants(get_in(args, ["subject_params", "event_id"]), activity)
|
notify_anonymous_participants(get_in(args, ["subject_params", "event_id"]), activity)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if args["subject"] == "conversation_created" do
|
||||||
|
notify_anonymous_participants(
|
||||||
|
get_in(args, ["subject_params", "conversation_event_id"]),
|
||||||
|
activity
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
args
|
args
|
||||||
|> users_to_notify(author_id: args["author_id"], group_id: Map.get(args, "group_id"))
|
|> users_to_notify(author_id: args["author_id"], group_id: Map.get(args, "group_id"))
|
||||||
|> Enum.each(&Notifier.notify(&1, activity, single_activity: true))
|
|> Enum.each(¬ify_user(&1, activity))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_activity(args) do
|
defp build_activity(args) do
|
||||||
|
@ -48,6 +57,15 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilder do
|
||||||
users_from_actor_ids(mentionned_actor_ids, Keyword.fetch!(options, :author_id))
|
users_from_actor_ids(mentionned_actor_ids, Keyword.fetch!(options, :author_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec users_to_notify(map(), Keyword.t()) :: list(Users.t())
|
||||||
|
defp users_to_notify(
|
||||||
|
%{"subject" => subject, "participant" => %{"actor_id" => actor_id}},
|
||||||
|
options
|
||||||
|
)
|
||||||
|
when subject in ["conversation_created", "conversation_replied"] do
|
||||||
|
users_from_actor_ids([actor_id], Keyword.fetch!(options, :author_id))
|
||||||
|
end
|
||||||
|
|
||||||
defp users_to_notify(
|
defp users_to_notify(
|
||||||
%{"subject" => "discussion_mention", "mentions" => mentionned_actor_ids},
|
%{"subject" => "discussion_mention", "mentions" => mentionned_actor_ids},
|
||||||
options
|
options
|
||||||
|
@ -114,4 +132,9 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilder do
|
||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp notify_user(user, activity) do
|
||||||
|
Logger.debug("Notifying #{user.email} for activity #{activity.subject}")
|
||||||
|
Notifier.notify(user, activity, single_activity: true)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -44,7 +44,7 @@ defmodule Mobilizon.Web.Auth.Context do
|
||||||
|
|
||||||
context = if is_nil(user_agent), do: context, else: Map.put(context, :user_agent, user_agent)
|
context = if is_nil(user_agent), do: context, else: Map.put(context, :user_agent, user_agent)
|
||||||
|
|
||||||
put_private(conn, :absinthe, %{context: context})
|
Absinthe.Plug.put_options(conn, context: context)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp set_user_context({conn, context}, %User{id: user_id, email: user_email} = user) do
|
defp set_user_context({conn, context}, %User{id: user_id, email: user_email} = user) do
|
||||||
|
|
20
lib/web/cache/activity_pub.ex
vendored
20
lib/web/cache/activity_pub.ex
vendored
|
@ -3,9 +3,10 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
|
||||||
ActivityPub related cache.
|
ActivityPub related cache.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias Mobilizon.{Actors, Discussions, Events, Posts, Resources, Todos, Tombstone}
|
alias Mobilizon.{Actors, Conversations, Discussions, Events, Posts, Resources, Todos, Tombstone}
|
||||||
alias Mobilizon.Actors.Actor, as: ActorModel
|
alias Mobilizon.Actors.Actor, as: ActorModel
|
||||||
alias Mobilizon.Actors.Member
|
alias Mobilizon.Actors.Member
|
||||||
|
alias Mobilizon.Conversations.Conversation
|
||||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||||
alias Mobilizon.Events.Event
|
alias Mobilizon.Events.Event
|
||||||
alias Mobilizon.Federation.ActivityPub.{Actor, Relay}
|
alias Mobilizon.Federation.ActivityPub.{Actor, Relay}
|
||||||
|
@ -184,6 +185,23 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets a conversation participant by it's ID, with all associations loaded.
|
||||||
|
"""
|
||||||
|
@spec get_conversation_by_id_with_preload(String.t()) ::
|
||||||
|
{:commit, Todo.t()} | {:ignore, nil}
|
||||||
|
def get_conversation_by_id_with_preload(id) do
|
||||||
|
Cachex.fetch(@cache, "conversation_participant_" <> id, fn "conversation_participant_" <> id ->
|
||||||
|
case Conversations.get_conversation_participant(id) do
|
||||||
|
%Conversation{} = conversation ->
|
||||||
|
{:commit, conversation}
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
{:ignore, nil}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets a member by its UUID, with all associations loaded.
|
Gets a member by its UUID, with all associations loaded.
|
||||||
"""
|
"""
|
||||||
|
|
5
lib/web/cache/cache.ex
vendored
5
lib/web/cache/cache.ex
vendored
|
@ -4,6 +4,7 @@ defmodule Mobilizon.Web.Cache do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias Mobilizon.Actors.{Actor, Member}
|
alias Mobilizon.Actors.{Actor, Member}
|
||||||
|
alias Mobilizon.Conversations.Conversation
|
||||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||||
alias Mobilizon.Events.Event
|
alias Mobilizon.Events.Event
|
||||||
alias Mobilizon.Posts.Post
|
alias Mobilizon.Posts.Post
|
||||||
|
@ -27,6 +28,10 @@ defmodule Mobilizon.Web.Cache do
|
||||||
defdelegate get_todo_list_by_uuid_with_preload(uuid), to: ActivityPub
|
defdelegate get_todo_list_by_uuid_with_preload(uuid), to: ActivityPub
|
||||||
@spec get_todo_by_uuid_with_preload(binary) :: {:commit, Todo.t()} | {:ignore, nil}
|
@spec get_todo_by_uuid_with_preload(binary) :: {:commit, Todo.t()} | {:ignore, nil}
|
||||||
defdelegate get_todo_by_uuid_with_preload(uuid), to: ActivityPub
|
defdelegate get_todo_by_uuid_with_preload(uuid), to: ActivityPub
|
||||||
|
|
||||||
|
@spec get_conversation_by_id_with_preload(binary) ::
|
||||||
|
{:commit, Conversation.t()} | {:ignore, nil}
|
||||||
|
defdelegate get_conversation_by_id_with_preload(uuid), to: ActivityPub
|
||||||
@spec get_member_by_uuid_with_preload(binary) :: {:commit, Member.t()} | {:ignore, nil}
|
@spec get_member_by_uuid_with_preload(binary) :: {:commit, Member.t()} | {:ignore, nil}
|
||||||
defdelegate get_member_by_uuid_with_preload(uuid), to: ActivityPub
|
defdelegate get_member_by_uuid_with_preload(uuid), to: ActivityPub
|
||||||
@spec get_post_by_slug_with_preload(binary) :: {:commit, Post.t()} | {:ignore, nil}
|
@spec get_post_by_slug_with_preload(binary) :: {:commit, Post.t()} | {:ignore, nil}
|
||||||
|
|
|
@ -13,9 +13,7 @@ defmodule Mobilizon.Web.GraphQLSocket do
|
||||||
with {:ok, authed_socket} <-
|
with {:ok, authed_socket} <-
|
||||||
Guardian.Phoenix.Socket.authenticate(socket, Mobilizon.Web.Auth.Guardian, token),
|
Guardian.Phoenix.Socket.authenticate(socket, Mobilizon.Web.Auth.Guardian, token),
|
||||||
resource <- Guardian.Phoenix.Socket.current_resource(authed_socket) do
|
resource <- Guardian.Phoenix.Socket.current_resource(authed_socket) do
|
||||||
set_context(authed_socket, resource)
|
{:ok, set_context(authed_socket, resource)}
|
||||||
|
|
||||||
{:ok, authed_socket}
|
|
||||||
else
|
else
|
||||||
{:error, _} ->
|
{:error, _} ->
|
||||||
:error
|
:error
|
||||||
|
@ -24,8 +22,17 @@ defmodule Mobilizon.Web.GraphQLSocket do
|
||||||
|
|
||||||
def connect(_args, _socket), do: :error
|
def connect(_args, _socket), do: :error
|
||||||
|
|
||||||
@spec id(any) :: nil
|
@spec id(Phoenix.Socket.t()) :: String.t() | nil
|
||||||
def id(_socket), do: nil
|
def id(%Phoenix.Socket{assigns: assigns}) do
|
||||||
|
context = Keyword.get(assigns.absinthe.opts, :context)
|
||||||
|
current_user = Map.get(context, :current_user)
|
||||||
|
|
||||||
|
if current_user do
|
||||||
|
"user_socket:#{current_user.id}"
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@spec set_context(Phoenix.Socket.t(), User.t() | ApplicationToken.t()) :: Phoenix.Socket.t()
|
@spec set_context(Phoenix.Socket.t(), User.t() | ApplicationToken.t()) :: Phoenix.Socket.t()
|
||||||
defp set_context(socket, %User{} = user) do
|
defp set_context(socket, %User{} = user) do
|
||||||
|
|
|
@ -85,6 +85,12 @@ defmodule Mobilizon.Web.PageController do
|
||||||
render_or_error(conn, &checks?/3, status, :todo, todo)
|
render_or_error(conn, &checks?/3, status, :todo, todo)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec conversation(Plug.Conn.t(), map()) :: Plug.Conn.t() | {:error, :not_found}
|
||||||
|
def conversation(conn, %{"id" => slug}) do
|
||||||
|
{status, conversation} = Cache.get_conversation_by_id_with_preload(slug)
|
||||||
|
render_or_error(conn, &checks?/3, status, :conversation, conversation)
|
||||||
|
end
|
||||||
|
|
||||||
@typep collections :: :resources | :posts | :discussions | :events | :todos
|
@typep collections :: :resources | :posts | :discussions | :events | :todos
|
||||||
|
|
||||||
@spec resources(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
@spec resources(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
|
|
|
@ -10,6 +10,7 @@ defmodule Mobilizon.Web.Email.Activity do
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Config
|
alias Mobilizon.Config
|
||||||
alias Mobilizon.Web.Email
|
alias Mobilizon.Web.Email
|
||||||
|
require Logger
|
||||||
|
|
||||||
@spec direct_activity(String.t(), list(), Keyword.t()) :: Swoosh.Email.t()
|
@spec direct_activity(String.t(), list(), Keyword.t()) :: Swoosh.Email.t()
|
||||||
def direct_activity(
|
def direct_activity(
|
||||||
|
@ -39,6 +40,36 @@ defmodule Mobilizon.Web.Email.Activity do
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec anonymous_activity(String.t(), Activity.t(), Keyword.t()) :: Swoosh.Email.t()
|
@spec anonymous_activity(String.t(), Activity.t(), Keyword.t()) :: Swoosh.Email.t()
|
||||||
|
def anonymous_activity(
|
||||||
|
email,
|
||||||
|
%Activity{subject_params: subject_params, type: :conversation} = activity,
|
||||||
|
options
|
||||||
|
) do
|
||||||
|
locale = Keyword.get(options, :locale, "en")
|
||||||
|
|
||||||
|
subject =
|
||||||
|
dgettext(
|
||||||
|
"activity",
|
||||||
|
"Informations about your event %{event}",
|
||||||
|
event: subject_params["conversation_event_title"]
|
||||||
|
)
|
||||||
|
|
||||||
|
conversation = Mobilizon.Conversations.get_conversation(activity.object_id)
|
||||||
|
|
||||||
|
Logger.debug("Going to send anonymous activity of type #{activity.type} to #{email}")
|
||||||
|
|
||||||
|
[to: email, subject: subject]
|
||||||
|
|> Email.base_email()
|
||||||
|
|> render_body(:email_anonymous_activity, %{
|
||||||
|
subject: subject,
|
||||||
|
activity: activity,
|
||||||
|
locale: locale,
|
||||||
|
extra: %{
|
||||||
|
"conversation" => conversation
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
def anonymous_activity(email, %Activity{subject_params: subject_params} = activity, options) do
|
def anonymous_activity(email, %Activity{subject_params: subject_params} = activity, options) do
|
||||||
locale = Keyword.get(options, :locale, "en")
|
locale = Keyword.get(options, :locale, "en")
|
||||||
|
|
||||||
|
@ -49,6 +80,8 @@ defmodule Mobilizon.Web.Email.Activity do
|
||||||
event: subject_params["event_title"]
|
event: subject_params["event_title"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Logger.debug("Going to send anonymous activity of type #{activity.type} to #{email}")
|
||||||
|
|
||||||
[to: email, subject: subject]
|
[to: email, subject: subject]
|
||||||
|> Email.base_email()
|
|> Email.base_email()
|
||||||
|> render_body(:email_anonymous_activity, %{
|
|> render_body(:email_anonymous_activity, %{
|
||||||
|
|
|
@ -10,6 +10,21 @@ defmodule Mobilizon.Web.Endpoint do
|
||||||
use Phoenix.Endpoint, otp_app: :mobilizon
|
use Phoenix.Endpoint, otp_app: :mobilizon
|
||||||
use Absinthe.Phoenix.Endpoint
|
use Absinthe.Phoenix.Endpoint
|
||||||
|
|
||||||
|
# Serve at "/" the static files from "priv/static" directory.
|
||||||
|
#
|
||||||
|
# You should set gzip to true if you are running phoenix.digest
|
||||||
|
# when deploying your static files in production.
|
||||||
|
plug(
|
||||||
|
Plug.Static,
|
||||||
|
at: "/",
|
||||||
|
from: {:mobilizon, "priv/static"},
|
||||||
|
gzip: false,
|
||||||
|
only: Mobilizon.Web.static_paths()
|
||||||
|
# only_matching: ["precache-manifest"]
|
||||||
|
)
|
||||||
|
|
||||||
|
plug(Mobilizon.Web.Plugs.UploadedMedia)
|
||||||
|
|
||||||
plug(Mobilizon.Web.Plugs.DetectLocalePlug)
|
plug(Mobilizon.Web.Plugs.DetectLocalePlug)
|
||||||
|
|
||||||
if Application.compile_env(:mobilizon, :env) !== :dev do
|
if Application.compile_env(:mobilizon, :env) !== :dev do
|
||||||
|
@ -37,21 +52,6 @@ defmodule Mobilizon.Web.Endpoint do
|
||||||
do: RemoteIp
|
do: RemoteIp
|
||||||
)
|
)
|
||||||
|
|
||||||
plug(Mobilizon.Web.Plugs.UploadedMedia)
|
|
||||||
|
|
||||||
# Serve at "/" the static files from "priv/static" directory.
|
|
||||||
#
|
|
||||||
# You should set gzip to true if you are running phoenix.digest
|
|
||||||
# when deploying your static files in production.
|
|
||||||
plug(
|
|
||||||
Plug.Static,
|
|
||||||
at: "/",
|
|
||||||
from: {:mobilizon, "priv/static"},
|
|
||||||
gzip: false,
|
|
||||||
only: Mobilizon.Web.static_paths(),
|
|
||||||
only_matching: ["precache-manifest"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Code reloading can be explicitly enabled under the
|
# Code reloading can be explicitly enabled under the
|
||||||
# :code_reloader configuration of your endpoint.
|
# :code_reloader configuration of your endpoint.
|
||||||
if code_reloading? do
|
if code_reloading? do
|
||||||
|
|
|
@ -72,7 +72,6 @@ defmodule Mobilizon.Web.Router do
|
||||||
|
|
||||||
pipeline :browser do
|
pipeline :browser do
|
||||||
plug(:put_request_context)
|
plug(:put_request_context)
|
||||||
plug(Plug.Static, at: "/", from: "priv/static")
|
|
||||||
|
|
||||||
plug(Mobilizon.Web.Plugs.SetLocalePlug)
|
plug(Mobilizon.Web.Plugs.SetLocalePlug)
|
||||||
|
|
||||||
|
@ -132,6 +131,7 @@ defmodule Mobilizon.Web.Router do
|
||||||
get("/@:name/discussions", PageController, :discussions)
|
get("/@:name/discussions", PageController, :discussions)
|
||||||
get("/@:name/events", PageController, :events)
|
get("/@:name/events", PageController, :events)
|
||||||
get("/p/:slug", PageController, :post)
|
get("/p/:slug", PageController, :post)
|
||||||
|
get("/conversations/:id", PageController, :conversation)
|
||||||
get("/@:name/c/:slug", PageController, :discussion)
|
get("/@:name/c/:slug", PageController, :discussion)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -176,6 +176,7 @@ defmodule Mobilizon.Web.Router do
|
||||||
|
|
||||||
forward("/", Absinthe.Plug.GraphiQL,
|
forward("/", Absinthe.Plug.GraphiQL,
|
||||||
schema: Mobilizon.GraphQL.Schema,
|
schema: Mobilizon.GraphQL.Schema,
|
||||||
|
socket: Mobilizon.Web.GraphQLSocket,
|
||||||
interface: :playground
|
interface: :playground
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<%= case @activity.subject do %>
|
||||||
|
<% :conversation_created -> %>
|
||||||
|
<%= dgettext("activity", "%{profile} mentionned you in a %{conversation}.", %{
|
||||||
|
profile: "<b>#{escaped_display_name_and_username(@activity.author)}</b>",
|
||||||
|
conversation:
|
||||||
|
"<a href=\"#{Routes.page_url(Mobilizon.Web.Endpoint,
|
||||||
|
:conversation,
|
||||||
|
@activity.subject_params["conversation_participant_id"]) |> URI.decode()}\">conversation</a>"
|
||||||
|
})
|
||||||
|
|> raw %>
|
||||||
|
<% :conversation_replied -> %>
|
||||||
|
<%= dgettext("activity", "%{profile} replied you in a %{conversation}.", %{
|
||||||
|
profile: "<b>#{escaped_display_name_and_username(@activity.author)}</b>",
|
||||||
|
conversation:
|
||||||
|
"<a href=\"#{Routes.page_url(Mobilizon.Web.Endpoint,
|
||||||
|
:conversation,
|
||||||
|
@activity.subject_params["conversation_participant_id"]) |> URI.decode()}\">conversation</a>"
|
||||||
|
})
|
||||||
|
|> raw %>
|
||||||
|
<% end %>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<%= case @activity.subject do %><% :conversation_created -> %><%= dgettext("activity", "%{profile} mentionned you in a conversation.",
|
||||||
|
%{
|
||||||
|
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
|
||||||
|
}
|
||||||
|
) %>
|
||||||
|
<%= Routes.page_url(Mobilizon.Web.Endpoint, :conversation, @activity.subject_params["conversation_participant_id"]) |> URI.decode() %><% :conversation_replied -> %><%= dgettext("activity", "%{profile} replied you in a conversation.",
|
||||||
|
%{
|
||||||
|
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
|
||||||
|
}
|
||||||
|
) %>
|
||||||
|
<%= Routes.page_url(Mobilizon.Web.Endpoint, :conversation, @activity.subject_params["conversation_participant_id"]) |> URI.decode() %><% end %>
|
|
@ -35,61 +35,164 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="600">
|
<td align="center" valign="top" width="600">
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
|
<%= case @activity.type do %>
|
||||||
<!-- COPY -->
|
<% :comment -> %>
|
||||||
<tr>
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
|
||||||
<td bgcolor="#ffffff" align="left">
|
<!-- COPY -->
|
||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
<tr>
|
||||||
<tr>
|
<td bgcolor="#ffffff" align="left">
|
||||||
<td
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
align="center"
|
<tr>
|
||||||
style="border-radius: 3px; text-align: left; padding: 10px 5% 0px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 16px; font-weight: 400;line-height: 25px;"
|
<td
|
||||||
>
|
align="center"
|
||||||
<%= dgettext(
|
style="border-radius: 3px; text-align: left; padding: 10px 5% 0px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 16px; font-weight: 400;line-height: 25px;"
|
||||||
"activity",
|
>
|
||||||
"%{profile} has posted an announcement under event %{event}.",
|
<%= dgettext(
|
||||||
%{
|
"activity",
|
||||||
profile: "<b>#{escape_html(display_name_and_username(@activity.author))}</b>",
|
"%{profile} has posted a public announcement under event %{event}.",
|
||||||
event:
|
%{
|
||||||
"<a href=\"#{Routes.page_url(Mobilizon.Web.Endpoint,
|
profile:
|
||||||
:event,
|
"<b>#{escape_html(display_name_and_username(@activity.author))}</b>",
|
||||||
@activity.subject_params["event_uuid"]) |> URI.decode()}\">
|
event:
|
||||||
|
"<a href=\"#{Routes.page_url(Mobilizon.Web.Endpoint,
|
||||||
|
:event,
|
||||||
|
@activity.subject_params["event_uuid"]) |> URI.decode()}\">
|
||||||
#{escape_html(@activity.subject_params["event_title"])}
|
#{escape_html(@activity.subject_params["event_title"])}
|
||||||
</a>"
|
</a>"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|> raw %>
|
|> raw %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td bgcolor="#ffffff" align="left">
|
<td bgcolor="#ffffff" align="left">
|
||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
|
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
|
||||||
<table border="0" cellspacing="0" cellpadding="0">
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="border-radius: 3px;" bgcolor="#3C376E">
|
<td align="center" style="border-radius: 3px;" bgcolor="#3C376E">
|
||||||
<a
|
<a
|
||||||
href={
|
href={
|
||||||
"#{Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"])}"
|
"#{Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"])}"
|
||||||
}
|
}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"
|
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"
|
||||||
>
|
>
|
||||||
<%= gettext("Visit event page") %>
|
<%= gettext("Visit event page") %>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
<% :conversation -> %>
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
|
||||||
|
<!-- COPY -->
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="left">
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="center"
|
||||||
|
style="border-radius: 3px; text-align: left; padding: 10px 5% 0px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 16px; font-weight: 400;line-height: 25px;"
|
||||||
|
>
|
||||||
|
<%= dgettext(
|
||||||
|
"activity",
|
||||||
|
"%{profile} has posted a private announcement about event %{event}.",
|
||||||
|
%{
|
||||||
|
profile:
|
||||||
|
"<b>#{escape_html(display_name_and_username(@activity.author))}</b>",
|
||||||
|
event:
|
||||||
|
"<a href=\"#{Routes.page_url(Mobilizon.Web.Endpoint,
|
||||||
|
:event,
|
||||||
|
@activity.subject_params["conversation_event_uuid"]) |> URI.decode()}\">
|
||||||
|
#{escape_html(@activity.subject_params["conversation_event_title"])}
|
||||||
|
</a>"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|> raw %>
|
||||||
|
<%= dgettext(
|
||||||
|
"activity",
|
||||||
|
"It might give details on how to join the event, so make sure to read it appropriately."
|
||||||
|
) %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="left">
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
|
||||||
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<blockquote style="border-left-width: 0.25rem;border-left-color: #e2e8f0;border-left-style: solid;padding-left: 1em;margin: 0;text-align: start;color: #474467;font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 16px; font-weight: 400;line-height: 25px;">
|
||||||
|
<%= @extra["conversation"].last_comment.text
|
||||||
|
|> sanitize_to_basic_html()
|
||||||
|
|> raw() %>
|
||||||
|
</blockquote>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="left">
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="center"
|
||||||
|
style="border-radius: 3px; text-align: left; padding: 10px 5% 0px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 16px; font-weight: 400;line-height: 25px;"
|
||||||
|
>
|
||||||
|
<%= dgettext(
|
||||||
|
"activity",
|
||||||
|
"This information is sent privately to you as a person who registered for this event. Share the informations above with other people with caution."
|
||||||
|
) %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="left">
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
|
||||||
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="border-radius: 3px;" bgcolor="#3C376E">
|
||||||
|
<a
|
||||||
|
href={
|
||||||
|
"#{Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["conversation_event_uuid"])}"
|
||||||
|
}
|
||||||
|
target="_blank"
|
||||||
|
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"
|
||||||
|
>
|
||||||
|
<%= gettext("Visit event page") %>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<% end %>
|
||||||
<!--[if (gte mso 9)|(IE)]>
|
<!--[if (gte mso 9)|(IE)]>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -1,11 +1,30 @@
|
||||||
<%= @subject %>
|
<%= @subject %>
|
||||||
|
|
||||||
==
|
==
|
||||||
|
<%= case @activity.type do %>
|
||||||
<%= dgettext("activity", "%{profile} has posted an announcement under event %{event}.",
|
<% :comment -> %>
|
||||||
|
<%= dgettext("activity", "%{profile} has posted a public announcement under event %{event}.",
|
||||||
%{
|
%{
|
||||||
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
|
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
|
||||||
event: @activity.subject_params["event_title"]
|
event: @activity.subject_params["event_title"]
|
||||||
}
|
}
|
||||||
) %>
|
) %>
|
||||||
<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %>
|
<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %>
|
||||||
|
<% :conversation -> %>
|
||||||
|
<%= dgettext("activity", "%{profile} has posted a private announcement about event %{event}.",
|
||||||
|
%{
|
||||||
|
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
|
||||||
|
event: @activity.subject_params["conversation_event_title"]
|
||||||
|
}
|
||||||
|
) %>
|
||||||
|
<%= dgettext("activity", "It might give details on how to join the event, so make sure to read it appropriately.") %>
|
||||||
|
|
||||||
|
--
|
||||||
|
|
||||||
|
<%= @extra["conversation"].last_comment.text |> html_to_text() |> mail_quote() %>
|
||||||
|
|
||||||
|
--
|
||||||
|
|
||||||
|
<%= dgettext("activity", "This information is sent privately to you as a person who registered for this event. Share the informations above with other people with caution.") %>
|
||||||
|
<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["conversation_event_uuid"]) |> URI.decode() %>
|
||||||
|
<% end %>
|
|
@ -167,6 +167,10 @@
|
||||||
<%= render("activity/_discussion_activity_item.html",
|
<%= render("activity/_discussion_activity_item.html",
|
||||||
activity: activity
|
activity: activity
|
||||||
) %>
|
) %>
|
||||||
|
<% :conversation -> %>
|
||||||
|
<%= render("activity/_conversation_activity_item.html",
|
||||||
|
activity: activity
|
||||||
|
) %>
|
||||||
<% :event -> %>
|
<% :event -> %>
|
||||||
<%= render("activity/_event_activity_item.html",
|
<%= render("activity/_event_activity_item.html",
|
||||||
activity: activity
|
activity: activity
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= for activity <- Enum.take(group_activities, 5) do %>
|
<%= for activity <- Enum.take(group_activities, 5) do %>
|
||||||
* <%= case activity.type do %><% :discussion -> %><%= render("activity/_discussion_activity_item.text", activity: activity) %><% :event -> %><%= render("activity/_event_activity_item.text", activity: activity) %><% :group -> %><%= render("activity/_group_activity_item.text", activity: activity) %>
|
* <%= case activity.type do %><% :discussion -> %><%= render("activity/_discussion_activity_item.text", activity: activity) %><% :conversation -> %><%= render("activity/_conversation_activity_item.text", activity: activity) %><% :event -> %><%= render("activity/_event_activity_item.text", activity: activity) %><% :group -> %><%= render("activity/_group_activity_item.text", activity: activity) %>
|
||||||
<% :member -> %><%= render("activity/_member_activity_item.text", activity: activity) %><% :post -> %><%= render("activity/_post_activity_item.text", activity: activity) %><% :resource -> %><%= render("activity/_resource_activity_item.text", activity: activity) %><% :comment -> %><%= render("activity/_comment_activity_item.text", activity: activity) %><% end %>
|
<% :member -> %><%= render("activity/_member_activity_item.text", activity: activity) %><% :post -> %><%= render("activity/_post_activity_item.text", activity: activity) %><% :resource -> %><%= render("activity/_resource_activity_item.text", activity: activity) %><% :comment -> %><%= render("activity/_comment_activity_item.text", activity: activity) %><% end %>
|
||||||
<%= unless @single_activity do %><%= datetime_to_string(activity.inserted_at, @locale, :short) %><% end %>
|
<%= unless @single_activity do %><%= datetime_to_string(activity.inserted_at, @locale, :short) %><% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -7,6 +7,7 @@ defmodule Mobilizon.Web.EmailView do
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Service.Address
|
alias Mobilizon.Service.Address
|
||||||
alias Mobilizon.Service.DateTime, as: DateTimeRenderer
|
alias Mobilizon.Service.DateTime, as: DateTimeRenderer
|
||||||
|
alias Mobilizon.Service.Formatter.{HTML, Text}
|
||||||
alias Mobilizon.Web.Router.Helpers, as: Routes
|
alias Mobilizon.Web.Router.Helpers, as: Routes
|
||||||
import Mobilizon.Web.Gettext
|
import Mobilizon.Web.Gettext
|
||||||
import Mobilizon.Service.Metadata.Utils, only: [process_description: 1]
|
import Mobilizon.Service.Metadata.Utils, only: [process_description: 1]
|
||||||
|
@ -35,6 +36,21 @@ defmodule Mobilizon.Web.EmailView do
|
||||||
|> safe_to_string()
|
|> safe_to_string()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec sanitize_to_basic_html(String.t()) :: String.t()
|
||||||
|
def sanitize_to_basic_html(html) do
|
||||||
|
case HTML.basic_html(html) do
|
||||||
|
{:ok, html} -> html
|
||||||
|
_ -> ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defdelegate html_to_text(html), to: HTML
|
||||||
|
|
||||||
|
def mail_quote(text) do
|
||||||
|
# https://www.emailonacid.com/blog/article/email-development/line-length-in-html-email/
|
||||||
|
Text.quote_paragraph(text, 78)
|
||||||
|
end
|
||||||
|
|
||||||
def escaped_display_name_and_username(actor) do
|
def escaped_display_name_and_username(actor) do
|
||||||
actor
|
actor
|
||||||
|> display_name_and_username()
|
|> display_name_and_username()
|
||||||
|
|
8
mix.exs
8
mix.exs
|
@ -258,17 +258,19 @@ defmodule Mobilizon.Mixfile do
|
||||||
"ecto.drop",
|
"ecto.drop",
|
||||||
"ecto.setup"
|
"ecto.setup"
|
||||||
],
|
],
|
||||||
test: [
|
prepare_test: [
|
||||||
"ecto.create",
|
"ecto.create",
|
||||||
"ecto.migrate",
|
"ecto.migrate",
|
||||||
"tz_world.update",
|
"tz_world.update"
|
||||||
|
],
|
||||||
|
test: [
|
||||||
&run_test/1
|
&run_test/1
|
||||||
],
|
],
|
||||||
"phx.deps_migrate_serve": [
|
"phx.deps_migrate_serve": [
|
||||||
"deps.get",
|
"deps.get",
|
||||||
"ecto.create --quiet",
|
"ecto.create --quiet",
|
||||||
"ecto.migrate",
|
"ecto.migrate",
|
||||||
"cmd cd js && yarn install && cd ../",
|
"cmd npm ci",
|
||||||
"phx.server"
|
"phx.server"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue