diff --git a/.buildpacks b/.buildpacks
new file mode 100644
index 000000000..31dd57096
--- /dev/null
+++ b/.buildpacks
@@ -0,0 +1 @@
+https://github.com/hashnuke/heroku-buildpack-elixir
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8b5131dc3..58c9de167 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -52,8 +52,7 @@ unit-testing:
- mix deps.get
- mix ecto.create
- mix ecto.migrate
- - mix test --trace --preload-modules
- - mix coveralls
+ - mix coveralls --trace --preload-modules
unit-testing-rum:
stage: test
@@ -95,3 +94,49 @@ docs-deploy:
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- rsync -hrvz --delete -e "ssh -p ${SSH_PORT}" priv/static/doc/ "${SSH_USER_HOST_LOCATION}/${CI_COMMIT_REF_NAME}"
+
+review_app:
+ image: alpine:3.9
+ stage: deploy
+ before_script:
+ - apk update && apk add openssh-client git
+ when: manual
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ url: https://$CI_ENVIRONMENT_SLUG.pleroma.online/
+ on_stop: stop_review_app
+ only:
+ - branches
+ except:
+ - master
+ - develop
+ script:
+ - echo "$CI_ENVIRONMENT_SLUG"
+ - mkdir -p ~/.ssh
+ - eval $(ssh-agent -s)
+ - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
+ - ssh-keyscan -H "pleroma.online" >> ~/.ssh/known_hosts
+ - (ssh -t dokku@pleroma.online -- apps:create "$CI_ENVIRONMENT_SLUG") || true
+ - ssh -t dokku@pleroma.online -- config:set "$CI_ENVIRONMENT_SLUG" APP_NAME="$CI_ENVIRONMENT_SLUG" APP_HOST="$CI_ENVIRONMENT_SLUG.pleroma.online" MIX_ENV=dokku
+ - (ssh -t dokku@pleroma.online -- postgres:create $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db) || true
+ - (ssh -t dokku@pleroma.online -- postgres:link $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db "$CI_ENVIRONMENT_SLUG") || true
+ - (ssh -t dokku@pleroma.online -- certs:add "$CI_ENVIRONMENT_SLUG" /home/dokku/server.crt /home/dokku/server.key) || true
+ - git push -f dokku@pleroma.online:$CI_ENVIRONMENT_SLUG $CI_COMMIT_SHA:refs/heads/master
+
+stop_review_app:
+ image: alpine:3.9
+ stage: deploy
+ before_script:
+ - apk update && apk add openssh-client git
+ when: manual
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ action: stop
+ script:
+ - echo "$CI_ENVIRONMENT_SLUG"
+ - mkdir -p ~/.ssh
+ - eval $(ssh-agent -s)
+ - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
+ - ssh-keyscan -H "pleroma.online" >> ~/.ssh/known_hosts
+ - ssh -t dokku@pleroma.online -- --force apps:destroy "$CI_ENVIRONMENT_SLUG"
+ - ssh -t dokku@pleroma.online -- --force postgres:destroy $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff1fff876..2144bbe28 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,7 +16,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mix Tasks: `mix pleroma.database remove_embedded_objects`
- Mix Tasks: `mix pleroma.database update_users_following_followers_counts`
- Mix Tasks: `mix pleroma.user toggle_confirmed`
+- Federation: Support for `Question` and `Answer` objects
- Federation: Support for reports
+- Configuration: `poll_limits` option
- Configuration: `safe_dm_mentions` option
- Configuration: `link_name` option
- Configuration: `fetch_initial_posts` option
@@ -37,6 +39,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension)
- Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/)
- Mastodon API: `POST /api/v1/accounts` (account creation API)
+- Mastodon API: [Polls](https://docs.joinmastodon.org/api/rest/polls/)
- ActivityPub C2S: OAuth endpoints
- Metadata: RelMe provider
- OAuth: added support for refresh tokens
diff --git a/Procfile b/Procfile
new file mode 100644
index 000000000..7ac187baa
--- /dev/null
+++ b/Procfile
@@ -0,0 +1,2 @@
+web: mix phx.server
+release: mix ecto.migrate
diff --git a/config/config.exs b/config/config.exs
index 68168b279..09c3be7de 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -208,6 +208,12 @@
avatar_upload_limit: 2_000_000,
background_upload_limit: 4_000_000,
banner_upload_limit: 4_000_000,
+ poll_limits: %{
+ max_options: 20,
+ max_option_chars: 200,
+ min_expiration: 0,
+ max_expiration: 365 * 24 * 60 * 60
+ },
registrations_open: true,
federating: true,
federation_reachability_timeout_days: 7,
diff --git a/config/dokku.exs b/config/dokku.exs
new file mode 100644
index 000000000..9ea0ec450
--- /dev/null
+++ b/config/dokku.exs
@@ -0,0 +1,25 @@
+use Mix.Config
+
+config :pleroma, Pleroma.Web.Endpoint,
+ http: [
+ port: String.to_integer(System.get_env("PORT") || "4000"),
+ protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192]
+ ],
+ protocol: "http",
+ secure_cookie_flag: false,
+ url: [host: System.get_env("APP_HOST"), scheme: "https", port: 443],
+ secret_key_base: "+S+ULgf7+N37c/lc9K66SMphnjQIRGklTu0BRr2vLm2ZzvK0Z6OH/PE77wlUNtvP"
+
+database_url =
+ System.get_env("DATABASE_URL") ||
+ raise """
+ environment variable DATABASE_URL is missing.
+ For example: ecto://USER:PASS@HOST/DATABASE
+ """
+
+config :pleroma, Pleroma.Repo,
+ # ssl: true,
+ url: database_url,
+ pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
+
+config :pleroma, :instance, name: "#{System.get_env("APP_NAME")} CI Instance"
diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md
index 4d99a2d2b..edc62727a 100644
--- a/docs/api/pleroma_api.md
+++ b/docs/api/pleroma_api.md
@@ -126,20 +126,6 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
## `/api/pleroma/admin/`…
See [Admin-API](Admin-API.md)
-## `/api/v1/pleroma/flavour/:flavour`
-* Method `POST`
-* Authentication: required
-* Response: JSON string. Returns the user flavour or the default one on success, otherwise returns `{"error": "error_msg"}`
-* Example response: "glitch"
-* Note: This is intended to be used only by mastofe
-
-## `/api/v1/pleroma/flavour`
-* Method `GET`
-* Authentication: required
-* Response: JSON string. Returns the user flavour or the default one.
-* Example response: "glitch"
-* Note: This is intended to be used only by mastofe
-
## `/api/pleroma/notifications/read`
### Mark a single notification as read
* Method `POST`
diff --git a/docs/config.md b/docs/config.md
index 67b062fe9..4797879d6 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -71,6 +71,11 @@ config :pleroma, Pleroma.Emails.Mailer,
* `avatar_upload_limit`: File size limit of user’s profile avatars
* `background_upload_limit`: File size limit of user’s profile backgrounds
* `banner_upload_limit`: File size limit of user’s profile banners
+* `poll_limits`: A map with poll limits for **local** polls
+ * `max_options`: Maximum number of options
+ * `max_option_chars`: Maximum number of characters per option
+ * `min_expiration`: Minimum expiration time (in seconds)
+ * `max_expiration`: Maximum expiration time (in seconds)
* `registrations_open`: Enable registrations for anyone, invitations can be enabled when false.
* `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`).
* `account_activation_required`: Require users to confirm their emails before signing in.
diff --git a/elixir_buildpack.config b/elixir_buildpack.config
new file mode 100644
index 000000000..c23b08fb8
--- /dev/null
+++ b/elixir_buildpack.config
@@ -0,0 +1,2 @@
+elixir_version=1.8.2
+erlang_version=21.3.7
diff --git a/installation/pleroma-mongooseim.cfg b/installation/pleroma-mongooseim.cfg
new file mode 100755
index 000000000..d7567321f
--- /dev/null
+++ b/installation/pleroma-mongooseim.cfg
@@ -0,0 +1,932 @@
+%%%
+%%% ejabberd configuration file
+%%%
+%%%'
+
+%%% The parameters used in this configuration file are explained in more detail
+%%% in the ejabberd Installation and Operation Guide.
+%%% Please consult the Guide in case of doubts, it is included with
+%%% your copy of ejabberd, and is also available online at
+%%% http://www.process-one.net/en/ejabberd/docs/
+
+%%% This configuration file contains Erlang terms.
+%%% In case you want to understand the syntax, here are the concepts:
+%%%
+%%% - The character to comment a line is %
+%%%
+%%% - Each term ends in a dot, for example:
+%%% override_global.
+%%%
+%%% - A tuple has a fixed definition, its elements are
+%%% enclosed in {}, and separated with commas:
+%%% {loglevel, 4}.
+%%%
+%%% - A list can have as many elements as you want,
+%%% and is enclosed in [], for example:
+%%% [http_poll, web_admin, tls]
+%%%
+%%% Pay attention that list elements are delimited with commas,
+%%% but no comma is allowed after the last list element. This will
+%%% give a syntax error unlike in more lenient languages (e.g. Python).
+%%%
+%%% - A keyword of ejabberd is a word in lowercase.
+%%% Strings are enclosed in "" and can contain spaces, dots, ...
+%%% {language, "en"}.
+%%% {ldap_rootdn, "dc=example,dc=com"}.
+%%%
+%%% - This term includes a tuple, a keyword, a list, and two strings:
+%%% {hosts, ["jabber.example.net", "im.example.com"]}.
+%%%
+%%% - This config is preprocessed during release generation by a tool which
+%%% interprets double curly braces as substitution markers, so avoid this
+%%% syntax in this file (though it's valid Erlang).
+%%%
+%%% So this is OK (though arguably looks quite ugly):
+%%% { {s2s_addr, "example-host.net"}, {127,0,0,1} }.
+%%%
+%%% And I can't give an example of what's not OK exactly because
+%%% of this rule.
+%%%
+
+
+%%%. =======================
+%%%' OVERRIDE STORED OPTIONS
+
+%%
+%% Override the old values stored in the database.
+%%
+
+%%
+%% Override global options (shared by all ejabberd nodes in a cluster).
+%%
+%%override_global.
+
+%%
+%% Override local options (specific for this particular ejabberd node).
+%%
+%%override_local.
+
+%%
+%% Remove the Access Control Lists before new ones are added.
+%%
+%%override_acls.
+
+
+%%%. =========
+%%%' DEBUGGING
+
+%%
+%% loglevel: Verbosity of log files generated by ejabberd.
+%% 0: No ejabberd log at all (not recommended)
+%% 1: Critical
+%% 2: Error
+%% 3: Warning
+%% 4: Info
+%% 5: Debug
+%%
+{loglevel, 3}.
+
+%%%. ================
+%%%' SERVED HOSTNAMES
+
+%%
+%% hosts: Domains served by ejabberd.
+%% You can define one or several, for example:
+%% {hosts, ["example.net", "example.com", "example.org"]}.
+%%
+{hosts, ["pleroma.soykaf.com"] }.
+
+%%
+%% route_subdomains: Delegate subdomains to other XMPP servers.
+%% For example, if this ejabberd serves example.org and you want
+%% to allow communication with an XMPP server called im.example.org.
+%%
+%%{route_subdomains, s2s}.
+
+
+%%%. ===============
+%%%' LISTENING PORTS
+
+%%
+%% listen: The ports ejabberd will listen on, which service each is handled
+%% by and what options to start it with.
+%%
+{listen,
+ [
+ %% BOSH and WS endpoints over HTTP
+ { 5280, ejabberd_cowboy, [
+ {num_acceptors, 10},
+ {transport_options, [{max_connections, 1024}]},
+ {modules, [
+
+ {"_", "/http-bind", mod_bosh},
+ {"_", "/ws-xmpp", mod_websockets, [{ejabberd_service, [
+ {access, all},
+ {shaper_rule, fast},
+ {ip, {127, 0, 0, 1}},
+ {password, "secret"}]}
+ %% Uncomment to enable connection dropping or/and server-side pings
+ %{timeout, 600000}, {ping_rate, 2000}
+ ]}
+ %% Uncomment to serve static files
+ %{"_", "/static/[...]", cowboy_static,
+ % {dir, "/var/www", [{mimetypes, cow_mimetypes, all}]}
+ %},
+
+ %% Example usage of mod_revproxy
+
+ %% {"_", "/[...]", mod_revproxy, [{timeout, 5000},
+ %% % time limit for upstream to respond
+ %% {body_length, 8000000},
+ %% % maximum body size (may be infinity)
+ %% {custom_headers, [{<<"header">>,<<"value">>}]}
+ %% % list of extra headers that are send to upstream
+ %% ]}
+
+ %% Example usage of mod_cowboy
+
+ %% {"_", "/[...]", mod_cowboy, [{http, mod_revproxy,
+ %% [{timeout, 5000},
+ %% % time limit for upstream to respond
+ %% {body_length, 8000000},
+ %% % maximum body size (may be infinity)
+ %% {custom_headers, [{<<"header">>,<<"value">>}]}
+ %% % list of extra headers that are send to upstream
+ %% ]},
+ %% {ws, xmpp, mod_websockets}
+ %% ]}
+ ]}
+ ]},
+
+ %% BOSH and WS endpoints over HTTPS
+ { 5285, ejabberd_cowboy, [
+ {num_acceptors, 10},
+ {transport_options, [{max_connections, 1024}]},
+ {ssl, [{certfile, "priv/ssl/fullchain.pem"}, {keyfile, "priv/ssl/privkey.pem"}, {password, ""}]},
+ {modules, [
+ {"_", "/http-bind", mod_bosh},
+ {"_", "/ws-xmpp", mod_websockets, [
+ %% Uncomment to enable connection dropping or/and server-side pings
+ %{timeout, 600000}, {ping_rate, 60000}
+ ]}
+ %% Uncomment to serve static files
+ %{"_", "/static/[...]", cowboy_static,
+ % {dir, "/var/www", [{mimetypes, cow_mimetypes, all}]}
+ %},
+ ]}
+ ]},
+
+ %% MongooseIM HTTP API it's important to start it on localhost
+ %% or some private interface only (not accessible from the outside)
+ %% At least start it on different port which will be hidden behind firewall
+
+ { {8088, "127.0.0.1"} , ejabberd_cowboy, [
+ {num_acceptors, 10},
+ {transport_options, [{max_connections, 1024}]},
+ {modules, [
+ {"localhost", "/api", mongoose_api_admin, []}
+ ]}
+ ]},
+
+ { 8089 , ejabberd_cowboy, [
+ {num_acceptors, 10},
+ {transport_options, [{max_connections, 1024}]},
+ {protocol_options, [{compress, true}]},
+ {ssl, [{certfile, "priv/ssl/fullchain.pem"}, {keyfile, "priv/ssl/privkey.pem"}, {password, ""}]},
+ {modules, [
+ {"_", "/api/sse", lasse_handler, [mongoose_client_api_sse]},
+ {"_", "/api/messages/[:with]", mongoose_client_api_messages, []},
+ {"_", "/api/contacts/[:jid]", mongoose_client_api_contacts, []},
+ {"_", "/api/rooms/[:id]", mongoose_client_api_rooms, []},
+ {"_", "/api/rooms/[:id]/config", mongoose_client_api_rooms_config, []},
+ {"_", "/api/rooms/:id/users/[:user]", mongoose_client_api_rooms_users, []},
+ {"_", "/api/rooms/[:id]/messages", mongoose_client_api_rooms_messages, []}
+ ]}
+ ]},
+
+ %% Following HTTP API is deprected, the new one abouve should be used instead
+
+ { {5288, "127.0.0.1"} , ejabberd_cowboy, [
+ {num_acceptors, 10},
+ {transport_options, [{max_connections, 1024}]},
+ {modules, [
+ {"localhost", "/api", mongoose_api, [{handlers, [mongoose_api_metrics,
+ mongoose_api_users]}]}
+ ]}
+ ]},
+
+ { 5222, ejabberd_c2s, [
+
+ %%
+ %% If TLS is compiled in and you installed a SSL
+ %% certificate, specify the full path to the
+ %% file and uncomment this line:
+ %%
+ {certfile, "priv/ssl/both.pem"}, starttls,
+
+ %%{zlib, 10000},
+ %% https://www.openssl.org/docs/apps/ciphers.html#CIPHER_STRINGS
+ %% {ciphers, "DEFAULT:!EXPORT:!LOW:!SSLv2"},
+ {access, c2s},
+ {shaper, c2s_shaper},
+ {max_stanza_size, 65536},
+ {protocol_options, ["no_sslv3"]}
+
+ ]},
+
+
+
+ %%
+ %% To enable the old SSL connection method on port 5223:
+ %%
+ %%{5223, ejabberd_c2s, [
+ %% {access, c2s},
+ %% {shaper, c2s_shaper},
+ %% {certfile, "/path/to/ssl.pem"}, tls,
+ %% {max_stanza_size, 65536}
+ %% ]},
+
+ { 5269, ejabberd_s2s_in, [
+ {shaper, s2s_shaper},
+ {max_stanza_size, 131072},
+ {protocol_options, ["no_sslv3"]}
+
+ ]}
+
+ %%
+ %% ejabberd_service: Interact with external components (transports, ...)
+ %%
+ ,{8888, ejabberd_service, [
+ {access, all},
+ {shaper_rule, fast},
+ {ip, {127, 0, 0, 1}},
+ {password, "secret"}
+ ]}
+
+ %%
+ %% ejabberd_stun: Handles STUN Binding requests
+ %%
+ %%{ {3478, udp}, ejabberd_stun, []}
+
+ ]}.
+
+%%
+%% s2s_use_starttls: Enable STARTTLS + Dialback for S2S connections.
+%% Allowed values are: false optional required required_trusted
+%% You must specify a certificate file.
+%%
+{s2s_use_starttls, optional}.
+%%
+%% s2s_certfile: Specify a certificate file.
+%%
+{s2s_certfile, "priv/ssl/both.pem"}.
+
+%% https://www.openssl.org/docs/apps/ciphers.html#CIPHER_STRINGS
+%% {s2s_ciphers, "DEFAULT:!EXPORT:!LOW:!SSLv2"}.
+
+%%
+%% domain_certfile: Specify a different certificate for each served hostname.
+%%
+%%{domain_certfile, "example.org", "/path/to/example_org.pem"}.
+%%{domain_certfile, "example.com", "/path/to/example_com.pem"}.
+
+%%
+%% S2S whitelist or blacklist
+%%
+%% Default s2s policy for undefined hosts.
+%%
+{s2s_default_policy, deny }.
+
+%%
+%% Allow or deny communication with specific servers.
+%%
+%%{ {s2s_host, "goodhost.org"}, allow}.
+%%{ {s2s_host, "badhost.org"}, deny}.
+
+{outgoing_s2s_port, 5269 }.
+
+%%
+%% IP addresses predefined for specific hosts to skip DNS lookups.
+%% Ports defined here take precedence over outgoing_s2s_port.
+%% Examples:
+%%
+%% { {s2s_addr, "example-host.net"}, {127,0,0,1} }.
+%% { {s2s_addr, "example-host.net"}, { {127,0,0,1}, 5269 } }.
+%% { {s2s_addr, "example-host.net"}, { {127,0,0,1}, 5269 } }.
+
+%%
+%% Outgoing S2S options
+%%
+%% Preferred address families (which to try first) and connect timeout
+%% in milliseconds.
+%%
+%%{outgoing_s2s_options, [ipv4, ipv6], 10000}.
+%%
+%%%. ==============
+%%%' SESSION BACKEND
+
+%%{sm_backend, {mnesia, []}}.
+
+%% Requires {redis, global, default, ..., ...} outgoing pool
+%%{sm_backend, {redis, []}}.
+
+{sm_backend, {mnesia, []} }.
+
+
+%%%. ==============
+%%%' AUTHENTICATION
+
+%% Advertised SASL mechanisms
+{sasl_mechanisms, [cyrsasl_plain]}.
+
+%%
+%% auth_method: Method used to authenticate the users.
+%% The default method is the internal.
+%% If you want to use a different method,
+%% comment this line and enable the correct ones.
+%%
+%% {auth_method, internal }.
+{auth_method, http }.
+{auth_opts, [
+ {http, global, auth, [{workers, 50}], [{server, "https://pleroma.soykaf.com"}]},
+ {password_format, plain} % default
+ %% {password_format, scram}
+
+ %% {scram_iterations, 4096} % default
+
+ %%
+ %% For auth_http:
+ %% {basic_auth, "user:password"}
+ %% {path_prefix, "/"} % default
+ %% auth_http requires {http, Host | global, auth, ..., ...} outgoing pool.
+ %%
+ %% For auth_external
+ %%{extauth_program, "/path/to/authentication/script"}.
+ %%
+ %% For auth_jwt
+ %% {jwt_secret_source, "/path/to/file"},
+ %% {jwt_algorithm, "RS256"},
+ %% {jwt_username_key, user}
+ %% For cyrsasl_external
+ %% {authenticate_with_cn, false}
+ {cyrsasl_external, standard}
+ ]}.
+
+%%
+%% Authentication using external script
+%% Make sure the script is executable by ejabberd.
+%%
+%%{auth_method, external}.
+
+%%
+%% Authentication using RDBMS
+%% Remember to setup a database in the next section.
+%%
+%%{auth_method, rdbms}.
+
+%%
+%% Authentication using LDAP
+%%
+%%{auth_method, ldap}.
+%%
+
+%% List of LDAP servers:
+%%{ldap_servers, ["localhost"]}.
+%%
+%% Encryption of connection to LDAP servers:
+%%{ldap_encrypt, none}.
+%%{ldap_encrypt, tls}.
+%%
+%% Port to connect to on LDAP servers:
+%%{ldap_port, 389}.
+%%{ldap_port, 636}.
+%%
+%% LDAP manager:
+%%{ldap_rootdn, "dc=example,dc=com"}.
+%%
+%% Password of LDAP manager:
+%%{ldap_password, "******"}.
+%%
+%% Search base of LDAP directory:
+%%{ldap_base, "dc=example,dc=com"}.
+%%
+%% LDAP attribute that holds user ID:
+%%{ldap_uids, [{"mail", "%u@mail.example.org"}]}.
+%%
+%% LDAP filter:
+%%{ldap_filter, "(objectClass=shadowAccount)"}.
+
+%%
+%% Anonymous login support:
+%% auth_method: anonymous
+%% anonymous_protocol: sasl_anon | login_anon | both
+%% allow_multiple_connections: true | false
+%%
+%%{host_config, "public.example.org", [{auth_method, anonymous},
+%% {allow_multiple_connections, false},
+%% {anonymous_protocol, sasl_anon}]}.
+%%
+%% To use both anonymous and internal authentication:
+%%
+%%{host_config, "public.example.org", [{auth_method, [internal, anonymous]}]}.
+
+
+%%%. ==============
+%%%' OUTGOING CONNECTIONS (e.g. DB)
+
+%% Here you may configure all outgoing connections used by MongooseIM,
+%% e.g. to RDBMS (such as MySQL), Riak or external HTTP components.
+%% Default MongooseIM configuration uses only Mnesia (non-Mnesia extensions are disabled),
+%% so no options here are uncommented out of the box.
+%% This section includes configuration examples; for comprehensive guide
+%% please consult MongooseIM documentation, page "Outgoing connections":
+%% - doc/advanced-configuration/outgoing-connections.md
+%% - https://mongooseim.readthedocs.io/en/latest/advanced-configuration/outgoing-connections/
+
+
+{outgoing_pools, [
+% {riak, global, default, [{workers, 5}], [{address, "127.0.0.1"}, {port, 8087}]},
+% {elastic, global, default, [], [{host, "elastic.host.com"}, {port, 9042}]},
+ {http, global, auth, [{workers, 50}], [{server, "https://pleroma.soykaf.com"}]}
+% {cassandra, global, default, [{workers, 100}], [{servers, [{"server1", 9042}]}, {keyspace, "big_mongooseim"}]},
+% {rdbms, global, default, [{workers, 10}], [{server, {mysql, "server", 3306, "database", "username", "password"}}]}
+]}.
+
+%% More examples that may be added to outgoing_pools list:
+%%
+%% == MySQL ==
+%% {rdbms, global, default, [{workers, 10}],
+%% [{server, {mysql, "server", 3306, "database", "username", "password"}},
+%% {keepalive_interval, 10}]},
+%% keepalive_interval is optional
+
+%% == PostgreSQL ==
+%% {rdbms, global, default, [{workers, 10}],
+%% [{server, {pgsql, "server", 5432, "database", "username", "password"}}]},
+
+%% == ODBC (MSSQL) ==
+%% {rdbms, global, default, [{workers, 10}],
+%% [{server, "DSN=mongooseim;UID=mongooseim;PWD=mongooseim"}]},
+
+%% == Elastic Search ==
+%% {elastic, global, default, [], [{host, "elastic.host.com"}, {port, 9042}]},
+
+%% == Riak ==
+%% {riak, global, default, [{workers, 20}], [{address, "127.0.0.1"}, {port, 8087}]},
+
+%% == HTTP ==
+%% {http, global, conn1, [{workers, 50}], [{server, "http://server:8080"}]},
+
+%% == Cassandra ==
+%% {cassandra, global, default, [{workers, 100}],
+%% [
+%% {servers, [
+%% {"cassandra_server1.example.com", 9042},
+%% {"cassandra_server2.example.com", 9042},
+%% {"cassandra_server3.example.com", 9042},
+%% {"cassandra_server4.example.com", 9042}
+%% ]},
+%% {keyspace, "big_mongooseim"}
+%% ]}
+
+%% == Extra options ==
+%%
+%% If you use PostgreSQL, have a large database, and need a
+%% faster but inexact replacement for "select count(*) from users"
+%%
+%%{pgsql_users_number_estimate, true}.
+%%
+%% rdbms_server_type specifies what database is used over the RDBMS layer
+%% Can take values mssql, pgsql, mysql
+%% In some cases (for example for MAM with pgsql) it is required to set proper value.
+%%
+%% {rdbms_server_type, pgsql}.
+
+%%%. ===============
+%%%' TRAFFIC SHAPERS
+
+%%
+%% The "normal" shaper limits traffic speed to 1000 B/s
+%%
+{shaper, normal, {maxrate, 1000}}.
+
+%%
+%% The "fast" shaper limits traffic speed to 50000 B/s
+%%
+{shaper, fast, {maxrate, 50000}}.
+
+%%
+%% This option specifies the maximum number of elements in the queue
+%% of the FSM. Refer to the documentation for details.
+%%
+{max_fsm_queue, 1000}.
+
+%%%. ====================
+%%%' ACCESS CONTROL LISTS
+
+%%
+%% The 'admin' ACL grants administrative privileges to XMPP accounts.
+%% You can put here as many accounts as you want.
+%%
+%{acl, admin, {user, "alice", "localhost"}}.
+%{acl, admin, {user, "a", "localhost"}}.
+
+%%
+%% Blocked users
+%%
+%%{acl, blocked, {user, "baduser", "example.org"}}.
+%%{acl, blocked, {user, "test"}}.
+
+%%
+%% Local users: don't modify this line.
+%%
+{acl, local, {user_regexp, ""}}.
+
+%%
+%% More examples of ACLs
+%%
+%%{acl, jabberorg, {server, "jabber.org"}}.
+%%{acl, aleksey, {user, "aleksey", "jabber.ru"}}.
+%%{acl, test, {user_regexp, "^test"}}.
+%%{acl, test, {user_glob, "test*"}}.
+
+%%
+%% Define specific ACLs in a virtual host.
+%%
+%%{host_config, "localhost",
+%% [
+%% {acl, admin, {user, "bob-local", "localhost"}}
+%% ]
+%%}.
+
+%%%. ============
+%%%' ACCESS RULES
+
+%% Maximum number of simultaneous sessions allowed for a single user:
+{access, max_user_sessions, [{10, all}]}.
+
+%% Maximum number of offline messages that users can have:
+{access, max_user_offline_messages, [{5000, admin}, {100, all}]}.
+
+%% This rule allows access only for local users:
+{access, local, [{allow, local}]}.
+
+%% Only non-blocked users can use c2s connections:
+{access, c2s, [{deny, blocked},
+ {allow, all}]}.
+
+%% For C2S connections, all users except admins use the "normal" shaper
+{access, c2s_shaper, [{none, admin},
+ {normal, all}]}.
+
+%% All S2S connections use the "fast" shaper
+{access, s2s_shaper, [{fast, all}]}.
+
+%% Admins of this server are also admins of the MUC service:
+{access, muc_admin, [{allow, admin}]}.
+
+%% Only accounts of the local ejabberd server can create rooms:
+{access, muc_create, [{allow, local}]}.
+
+%% All users are allowed to use the MUC service:
+{access, muc, [{allow, all}]}.
+
+%% In-band registration allows registration of any possible username.
+%% To disable in-band registration, replace 'allow' with 'deny'.
+{access, register, [{allow, all}]}.
+
+%% By default the frequency of account registrations from the same IP
+%% is limited to 1 account every 10 minutes. To disable, specify: infinity
+{registration_timeout, infinity}.
+
+%% Default settings for MAM.
+%% To set non-standard value, replace 'default' with 'allow' or 'deny'.
+%% Only user can access his/her archive by default.
+%% An online user can read room's archive by default.
+%% Only an owner can change settings and purge messages by default.
+%% Empty list (i.e. `[]`) means `[{deny, all}]`.
+{access, mam_set_prefs, [{default, all}]}.
+{access, mam_get_prefs, [{default, all}]}.
+{access, mam_lookup_messages, [{default, all}]}.
+{access, mam_purge_single_message, [{default, all}]}.
+{access, mam_purge_multiple_messages, [{default, all}]}.
+
+%% 1 command of the specified type per second.
+{shaper, mam_shaper, {maxrate, 1}}.
+%% This shaper is primeraly for Mnesia overload protection during stress testing.
+%% The limit is 1000 operations of each type per second.
+{shaper, mam_global_shaper, {maxrate, 1000}}.
+
+{access, mam_set_prefs_shaper, [{mam_shaper, all}]}.
+{access, mam_get_prefs_shaper, [{mam_shaper, all}]}.
+{access, mam_lookup_messages_shaper, [{mam_shaper, all}]}.
+{access, mam_purge_single_message_shaper, [{mam_shaper, all}]}.
+{access, mam_purge_multiple_messages_shaper, [{mam_shaper, all}]}.
+
+{access, mam_set_prefs_global_shaper, [{mam_global_shaper, all}]}.
+{access, mam_get_prefs_global_shaper, [{mam_global_shaper, all}]}.
+{access, mam_lookup_messages_global_shaper, [{mam_global_shaper, all}]}.
+{access, mam_purge_single_message_global_shaper, [{mam_global_shaper, all}]}.
+{access, mam_purge_multiple_messages_global_shaper, [{mam_global_shaper, all}]}.
+
+%%
+%% Define specific Access Rules in a virtual host.
+%%
+%%{host_config, "localhost",
+%% [
+%% {access, c2s, [{allow, admin}, {deny, all}]},
+%% {access, register, [{deny, all}]}
+%% ]
+%%}.
+
+%%%. ================
+%%%' DEFAULT LANGUAGE
+
+%%
+%% language: Default language used for server messages.
+%%
+{language, "en"}.
+
+%%
+%% Set a different default language in a virtual host.
+%%
+%%{host_config, "localhost",
+%% [{language, "ru"}]
+%%}.
+
+%%%. ================
+%%%' MISCELLANEOUS
+
+{all_metrics_are_global, false }.
+
+%%%. ========
+%%%' SERVICES
+
+%% Unlike modules, services are started per node and provide either features which are not
+%% related to any particular host, or backend stuff which is used by modules.
+%% This is handled by `mongoose_service` module.
+
+{services,
+ [
+ {service_admin_extra, [{submods, [node, accounts, sessions, vcard,
+ roster, last, private, stanza, stats]}]}
+ ]
+}.
+
+%%%. =======
+%%%' MODULES
+
+%%
+%% Modules enabled in all mongooseim virtual hosts.
+%% For list of possible modules options, check documentation.
+%%
+{modules,
+ [
+
+ %% The format for a single route is as follows:
+ %% {Host, Path, Method, Upstream}
+ %%
+ %% "_" can be used as wildcard for Host, Path and Method
+ %% Upstream can be either host (just http(s)://host:port) or uri
+ %% The difference is that host upstreams append whole path while
+ %% uri upstreams append only remainder that follows the matched Path
+ %% (this behaviour is similar to nginx's proxy_pass rules)
+ %%
+ %% Bindings can be used to match certain parts of host or path.
+ %% They will be later overlaid with parts of the upstream uri.
+ %%
+ %% {mod_revproxy,
+ %% [{routes, [{"www.erlang-solutions.com", "/admin", "_",
+ %% "https://www.erlang-solutions.com/"},
+ %% {":var.com", "/:var", "_", "http://localhost:8080/"},
+ %% {":domain.com", "/", "_", "http://localhost:8080/:domain"}]
+ %% }]},
+
+% {mod_http_upload, [
+ %% Set max file size in bytes. Defaults to 10 MB.
+ %% Disabled if value is `undefined`.
+% {max_file_size, 1024},
+ %% Use S3 storage backend
+% {backend, s3},
+ %% Set options for S3 backend
+% {s3, [
+% {bucket_url, "http://s3-eu-west-1.amazonaws.com/konbucket2"},
+% {region, "eu-west-1"},
+% {access_key_id, "AKIAIAOAONIULXQGMOUA"},
+% {secret_access_key, "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"}
+% ]}
+% ]},
+
+ {mod_adhoc, []},
+
+ {mod_disco, [{users_can_see_hidden_services, false}]},
+ {mod_commands, []},
+ {mod_muc_commands, []},
+ {mod_muc_light_commands, []},
+ {mod_last, []},
+ {mod_stream_management, [
+ % default 100
+ % size of a buffer of unacked messages
+ % {buffer_max, 100}
+
+ % default 1 - server sends the ack request after each stanza
+ % {ack_freq, 1}
+
+ % default: 600 seconds
+ % {resume_timeout, 600}
+ ]},
+ %% {mod_muc_light, [{host, "muclight.@HOST@"}]},
+ %% {mod_muc, [{host, "muc.@HOST@"},
+ %% {access, muc},
+ %% {access_create, muc_create}
+ %% ]},
+ %% {mod_muc_log, [
+ %% {outdir, "/tmp/muclogs"},
+ %% {access_log, muc}
+ %% ]},
+ {mod_offline, [{access_max_user_messages, max_user_offline_messages}]},
+ {mod_privacy, []},
+ {mod_blocking, []},
+ {mod_private, []},
+% {mod_private, [{backend, mnesia}]},
+% {mod_private, [{backend, rdbms}]},
+% {mod_register, [
+% %%
+% %% Set the minimum informational entropy for passwords.
+% %%
+% %%{password_strength, 32},
+%
+% %%
+% %% After successful registration, the user receives
+% %% a message with this subject and body.
+% %%
+% {welcome_message, {""}},
+%
+% %%
+% %% When a user registers, send a notification to
+% %% these XMPP accounts.
+% %%
+%
+%
+% %%
+% %% Only clients in the server machine can register accounts
+% %%
+% {ip_access, [{allow, "127.0.0.0/8"},
+% {deny, "0.0.0.0/0"}]},
+%
+% %%
+% %% Local c2s or remote s2s users cannot register accounts
+% %%
+% %%{access_from, deny},
+%
+% {access, register}
+% ]},
+ {mod_roster, []},
+ {mod_sic, []},
+ {mod_vcard, [%{matches, 1},
+%{search, true},
+%{ldap_search_operator, 'or'}, %% either 'or' or 'and'
+%{ldap_binary_search_fields, [<<"PHOTO">>]},
+%% list of binary search fields (as in vcard after mapping)
+{host, "vjud.@HOST@"}
+]},
+ {mod_bosh, []},
+ {mod_carboncopy, []}
+
+ %%
+ %% Message Archive Management (MAM, XEP-0313) for registered users and
+ %% Multi-User chats (MUCs).
+ %%
+
+% {mod_mam_meta, [
+ %% Use RDBMS backend (default)
+% {backend, rdbms},
+
+ %% Do not store user preferences (default)
+% {user_prefs_store, false},
+ %% Store user preferences in RDBMS
+% {user_prefs_store, rdbms},
+ %% Store user preferences in Mnesia (recommended).
+ %% The preferences store will be called each time, as a message is routed.
+ %% That is why Mnesia is better suited for this job.
+% {user_prefs_store, mnesia},
+
+ %% Enables a pool of asynchronous writers. (default)
+ %% Messages will be grouped together based on archive id.
+% {async_writer, true},
+
+ %% Cache information about users (default)
+% {cache_users, true},
+
+ %% Enable archivization for private messages (default)
+% {pm, [
+ %% Top-level options can be overriden here if needed, for example:
+% {async_writer, false}
+% ]},
+
+ %%
+ %% Message Archive Management (MAM) for multi-user chats (MUC).
+ %% Enable XEP-0313 for "muc.@HOST@".
+ %%
+% {muc, [
+% {host, "muc.@HOST@"}
+ %% As with pm, top-level options can be overriden for MUC archive
+% ]},
+%
+ %% Do not use a
umu
", + "url": "https://mastodon.sdf.org/@rinpatch", + "manuallyApprovesFollowers": false, + "publicKey": { + "id": "https://mastodon.sdf.org/users/rinpatch#main-key", + "owner": "https://mastodon.sdf.org/users/rinpatch", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1vbhYKDopb5xzfJB2TZY\n0ZvgxqdAhbSKKkQC5Q2b0ofhvueDy2AuZTnVk1/BbHNlqKlwhJUSpA6LiTZVvtcc\nMn6cmSaJJEg30gRF5GARP8FMcuq8e2jmceiW99NnUX17MQXsddSf2JFUwD0rUE8H\nBsgD7UzE9+zlA/PJOTBO7fvBEz9PTQ3r4sRMTJVFvKz2MU/U+aRNTuexRKMMPnUw\nfp6VWh1F44VWJEQOs4tOEjGiQiMQh5OfBk1w2haT3vrDbQvq23tNpUP1cRomLUtx\nEBcGKi5DMMBzE1RTVT1YUykR/zLWlA+JSmw7P6cWtsHYZovs8dgn8Po3X//6N+ng\nTQIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "tag": [], + "attachment": [], + "endpoints": { + "sharedInbox": "https://mastodon.sdf.org/inbox" + }, + "icon": { + "type": "Image", + "mediaType": "image/jpeg", + "url": "https://mastodon.sdf.org/system/accounts/avatars/000/067/580/original/bf05521bf711b7a0.jpg?1533238802" + }, + "image": { + "type": "Image", + "mediaType": "image/gif", + "url": "https://mastodon.sdf.org/system/accounts/headers/000/067/580/original/a99b987e798f7063.gif?1533278217" + } +} diff --git a/test/fixtures/mastodon-question-activity.json b/test/fixtures/mastodon-question-activity.json new file mode 100644 index 000000000..ac329c7d5 --- /dev/null +++ b/test/fixtures/mastodon-question-activity.json @@ -0,0 +1,99 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + } + } + ], + "id": "https://mastodon.sdf.org/users/rinpatch/statuses/102070944809637304/activity", + "type": "Create", + "actor": "https://mastodon.sdf.org/users/rinpatch", + "published": "2019-05-10T09:03:36Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://mastodon.sdf.org/users/rinpatch/followers" + ], + "object": { + "id": "https://mastodon.sdf.org/users/rinpatch/statuses/102070944809637304", + "type": "Question", + "summary": null, + "inReplyTo": null, + "published": "2019-05-10T09:03:36Z", + "url": "https://mastodon.sdf.org/@rinpatch/102070944809637304", + "attributedTo": "https://mastodon.sdf.org/users/rinpatch", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://mastodon.sdf.org/users/rinpatch/followers" + ], + "sensitive": false, + "atomUri": "https://mastodon.sdf.org/users/rinpatch/statuses/102070944809637304", + "inReplyToAtomUri": null, + "conversation": "tag:mastodon.sdf.org,2019-05-10:objectId=15095122:objectType=Conversation", + "content": "Why is Tenshi eating a corndog so cute?
", + "contentMap": { + "en": "Why is Tenshi eating a corndog so cute?
" + }, + "endTime": "2019-05-11T09:03:36Z", + "closed": "2019-05-11T09:03:36Z", + "attachment": [], + "tag": [], + "replies": { + "id": "https://mastodon.sdf.org/users/rinpatch/statuses/102070944809637304/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "partOf": "https://mastodon.sdf.org/users/rinpatch/statuses/102070944809637304/replies", + "items": [] + } + }, + "oneOf": [ + { + "type": "Note", + "name": "Dunno", + "replies": { + "type": "Collection", + "totalItems": 0 + } + }, + { + "type": "Note", + "name": "Everyone knows that!", + "replies": { + "type": "Collection", + "totalItems": 1 + } + }, + { + "type": "Note", + "name": "25 char limit is dumb", + "replies": { + "type": "Collection", + "totalItems": 0 + } + }, + { + "type": "Note", + "name": "I can't even fit a funny", + "replies": { + "type": "Collection", + "totalItems": 1 + } + } + ] + } +} diff --git a/test/fixtures/mastodon-vote.json b/test/fixtures/mastodon-vote.json new file mode 100644 index 000000000..c2c5f40c0 --- /dev/null +++ b/test/fixtures/mastodon-vote.json @@ -0,0 +1,16 @@ +{ + "@context": "https://www.w3.org/ns/activitystreams", + "actor": "https://mastodon.sdf.org/users/rinpatch", + "id": "https://mastodon.sdf.org/users/rinpatch#votes/387/activity", + "nickname": "rin", + "object": { + "attributedTo": "https://mastodon.sdf.org/users/rinpatch", + "id": "https://mastodon.sdf.org/users/rinpatch#votes/387", + "inReplyTo": "https://testing.uguu.ltd/objects/9d300947-2dcb-445d-8978-9a3b4b84fa14", + "name": "suya..", + "to": "https://testing.uguu.ltd/users/rin", + "type": "Note" + }, + "to": "https://testing.uguu.ltd/users/rin", + "type": "Create" +} diff --git a/test/notification_test.exs b/test/notification_test.exs index 581db58a8..be292abd9 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -78,33 +78,6 @@ test "it doesn't create a notification for an activity from a muted thread" do assert nil == Notification.create_notification(activity, muter) end - test "it disables notifications from people on remote instances" do - user = insert(:user, info: %{notification_settings: %{"remote" => false}}) - other_user = insert(:user) - - create_activity = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "actor" => other_user.ap_id, - "object" => %{ - "type" => "Note", - "content" => "Hi @#{user.nickname}", - "attributedTo" => other_user.ap_id - } - } - - {:ok, %{local: false} = activity} = Transmogrifier.handle_incoming(create_activity) - assert nil == Notification.create_notification(activity, user) - end - - test "it disables notifications from people on the local instance" do - user = insert(:user, info: %{notification_settings: %{"local" => false}}) - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hey @#{user.nickname}"}) - assert nil == Notification.create_notification(activity, user) - end - test "it disables notifications from followers" do follower = insert(:user) followed = insert(:user, info: %{notification_settings: %{"followers" => false}}) @@ -113,6 +86,13 @@ test "it disables notifications from followers" do assert nil == Notification.create_notification(activity, followed) end + test "it disables notifications from non-followers" do + follower = insert(:user) + followed = insert(:user, info: %{notification_settings: %{"non_followers" => false}}) + {:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"}) + assert nil == Notification.create_notification(activity, followed) + end + test "it disables notifications from people the user follows" do follower = insert(:user, info: %{notification_settings: %{"follows" => false}}) followed = insert(:user) @@ -122,6 +102,13 @@ test "it disables notifications from people the user follows" do assert nil == Notification.create_notification(activity, follower) end + test "it disables notifications from people the user does not follow" do + follower = insert(:user, info: %{notification_settings: %{"non_follows" => false}}) + followed = insert(:user) + {:ok, activity} = CommonAPI.post(followed, %{"status" => "hey @#{follower.nickname}"}) + assert nil == Notification.create_notification(activity, follower) + end + test "it doesn't create a notification for user if he is the activity author" do activity = insert(:note_activity) author = User.get_cached_by_ap_id(activity.data["actor"]) diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 66d7d5ba9..36b9265e7 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -52,6 +52,14 @@ def get("https://mastodon.social/users/emelie", _, _, _) do }} end + def get("https://mastodon.sdf.org/users/rinpatch", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/rinpatch.json") + }} + end + def get( "https://mastodon.social/.well-known/webfinger?resource=https://mastodon.social/users/emelie", _, diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 30adfda36..8b3233729 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do alias Pleroma.User alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.UserView + alias Pleroma.Web.ActivityPub.Utils setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -234,13 +235,17 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do end describe "/users/:nickname/inbox" do - test "it inserts an incoming activity into the database", %{conn: conn} do - user = insert(:user) - + setup do data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() - |> Map.put("bcc", [user.ap_id]) + + [data: data] + end + + test "it inserts an incoming activity into the database", %{conn: conn, data: data} do + user = insert(:user) + data = Map.put(data, "bcc", [user.ap_id]) conn = conn @@ -253,16 +258,15 @@ test "it inserts an incoming activity into the database", %{conn: conn} do assert Activity.get_by_ap_id(data["id"]) end - test "it accepts messages from actors that are followed by the user", %{conn: conn} do + test "it accepts messages from actors that are followed by the user", %{ + conn: conn, + data: data + } do recipient = insert(:user) actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"}) {:ok, recipient} = User.follow(recipient, actor) - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - object = data["object"] |> Map.put("attributedTo", actor.ap_id) @@ -309,13 +313,9 @@ test "it returns a note activity in a collection", %{conn: conn} do assert response(conn, 200) =~ note_activity.data["object"]["content"] end - test "it clears `unreachable` federation status of the sender", %{conn: conn} do + test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - |> Map.put("bcc", [user.ap_id]) + data = Map.put(data, "bcc", [user.ap_id]) sender_host = URI.parse(data["actor"]).host Instances.set_consistently_unreachable(sender_host) @@ -330,6 +330,47 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do assert "ok" == json_response(conn, 200) assert Instances.reachable?(sender_host) end + + test "it removes all follower collections but actor's", %{conn: conn} do + [actor, recipient] = insert_pair(:user) + + data = + File.read!("test/fixtures/activitypub-client-post-activity.json") + |> Poison.decode!() + + object = Map.put(data["object"], "attributedTo", actor.ap_id) + + data = + data + |> Map.put("id", Utils.generate_object_id()) + |> Map.put("actor", actor.ap_id) + |> Map.put("object", object) + |> Map.put("cc", [ + recipient.follower_address, + actor.follower_address + ]) + |> Map.put("to", [ + recipient.ap_id, + recipient.follower_address, + "https://www.w3.org/ns/activitystreams#Public" + ]) + + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{recipient.nickname}/inbox", data) + |> json_response(200) + + activity = Activity.get_by_ap_id(data["id"]) + + assert activity.id + assert actor.follower_address in activity.recipients + assert actor.follower_address in activity.data["cc"] + + refute recipient.follower_address in activity.recipients + refute recipient.follower_address in activity.data["cc"] + refute recipient.follower_address in activity.data["to"] + end end describe "/users/:nickname/outbox" do diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index ee71de8d0..89c8f79c9 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -113,6 +113,55 @@ test "it works for incoming notices with hashtags" do assert Enum.at(object.data["tag"], 2) == "moo" end + test "it works for incoming questions" do + data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!() + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + object = Object.normalize(activity) + + assert Enum.all?(object.data["oneOf"], fn choice -> + choice["name"] in [ + "Dunno", + "Everyone knows that!", + "25 char limit is dumb", + "I can't even fit a funny" + ] + end) + end + + test "it rewrites Note votes to Answers and increments vote counters on question activities" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "suya...", + "poll" => %{"options" => ["suya", "suya.", "suya.."], "expires_in" => 10} + }) + + object = Object.normalize(activity) + + data = + File.read!("test/fixtures/mastodon-vote.json") + |> Poison.decode!() + |> Kernel.put_in(["to"], user.ap_id) + |> Kernel.put_in(["object", "inReplyTo"], object.data["id"]) + |> Kernel.put_in(["object", "to"], user.ap_id) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + answer_object = Object.normalize(activity) + assert answer_object.data["type"] == "Answer" + object = Object.get_by_ap_id(object.data["id"]) + + assert Enum.any?( + object.data["oneOf"], + fn + %{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true + _ -> false + end + ) + end + test "it works for incoming notices with contentMap" do data = File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!() @@ -1210,10 +1259,37 @@ test "successfully reserializes a message with AS2 objects in IR" do end end - describe "fix_explicit_addressing" do - test "moves non-explicitly mentioned actors to cc" do - user = insert(:user) + test "Rewrites Answers to Notes" do + user = insert(:user) + {:ok, poll_activity} = + CommonAPI.post(user, %{ + "status" => "suya...", + "poll" => %{"options" => ["suya", "suya.", "suya.."], "expires_in" => 10} + }) + + poll_object = Object.normalize(poll_activity) + # TODO: Replace with CommonAPI vote creation when implemented + data = + File.read!("test/fixtures/mastodon-vote.json") + |> Poison.decode!() + |> Kernel.put_in(["to"], user.ap_id) + |> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"]) + |> Kernel.put_in(["object", "to"], user.ap_id) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + + assert data["object"]["type"] == "Note" + end + + describe "fix_explicit_addressing" do + setup do + user = insert(:user) + [user: user] + end + + test "moves non-explicitly mentioned actors to cc", %{user: user} do explicitly_mentioned_actors = [ "https://pleroma.gold/users/user1", "https://pleroma.gold/user2" @@ -1235,9 +1311,7 @@ test "moves non-explicitly mentioned actors to cc" do assert "https://social.beepboop.ga/users/dirb" in fixed_object["cc"] end - test "does not move actor's follower collection to cc" do - user = insert(:user) - + test "does not move actor's follower collection to cc", %{user: user} do object = %{ "actor" => user.ap_id, "to" => [user.follower_address], @@ -1248,5 +1322,21 @@ test "does not move actor's follower collection to cc" do assert user.follower_address in fixed_object["to"] refute user.follower_address in fixed_object["cc"] end + + test "removes recipient's follower collection from cc", %{user: user} do + recipient = insert(:user) + + object = %{ + "actor" => user.ap_id, + "to" => [recipient.ap_id, "https://www.w3.org/ns/activitystreams#Public"], + "cc" => [user.follower_address, recipient.follower_address] + } + + fixed_object = Transmogrifier.fix_explicit_addressing(object) + + assert user.follower_address in fixed_object["cc"] + refute recipient.follower_address in fixed_object["cc"] + refute recipient.follower_address in fixed_object["to"] + end end end diff --git a/test/web/activity_pub/visibilty_test.exs b/test/web/activity_pub/visibilty_test.exs index e2584f635..466d980dc 100644 --- a/test/web/activity_pub/visibilty_test.exs +++ b/test/web/activity_pub/visibilty_test.exs @@ -117,4 +117,8 @@ test "get_visibility", %{ assert Visibility.get_visibility(direct) == "direct" assert Visibility.get_visibility(unlisted) == "unlisted" end + + test "get_visibility with directMessage flag" do + assert Visibility.get_visibility(%{data: %{"directMessage" => true}}) == "direct" + end end diff --git a/test/web/mastodon_api/account_view_test.exs b/test/web/mastodon_api/account_view_test.exs index aaf2261bb..23f250990 100644 --- a/test/web/mastodon_api/account_view_test.exs +++ b/test/web/mastodon_api/account_view_test.exs @@ -78,10 +78,10 @@ test "Represent the user account for the account owner" do user = insert(:user) notification_settings = %{ - "remote" => true, - "local" => true, "followers" => true, - "follows" => true + "follows" => true, + "non_follows" => true, + "non_followers" => true } privacy = user.info.default_scope diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 93ef630f2..e941aae5b 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -146,6 +146,103 @@ test "posting a status", %{conn: conn} do refute id == third_id end + describe "posting polls" do + test "posting a poll", %{conn: conn} do + user = insert(:user) + time = NaiveDateTime.utc_now() + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "status" => "Who is the #bestgrill?", + "poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420} + }) + + response = json_response(conn, 200) + + assert Enum.all?(response["poll"]["options"], fn %{"title" => title} -> + title in ["Rei", "Asuka", "Misato"] + end) + + assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430 + refute response["poll"]["expred"] + end + + test "option limit is enforced", %{conn: conn} do + user = insert(:user) + limit = Pleroma.Config.get([:instance, :poll_limits, :max_options]) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "status" => "desu~", + "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1} + }) + + %{"error" => error} = json_response(conn, 422) + assert error == "Poll can't contain more than #{limit} options" + end + + test "option character limit is enforced", %{conn: conn} do + user = insert(:user) + limit = Pleroma.Config.get([:instance, :poll_limits, :max_option_chars]) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "status" => "...", + "poll" => %{ + "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)], + "expires_in" => 1 + } + }) + + %{"error" => error} = json_response(conn, 422) + assert error == "Poll options cannot be longer than #{limit} characters each" + end + + test "minimal date limit is enforced", %{conn: conn} do + user = insert(:user) + limit = Pleroma.Config.get([:instance, :poll_limits, :min_expiration]) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "status" => "imagine arbitrary limits", + "poll" => %{ + "options" => ["this post was made by pleroma gang"], + "expires_in" => limit - 1 + } + }) + + %{"error" => error} = json_response(conn, 422) + assert error == "Expiration date is too soon" + end + + test "maximum date limit is enforced", %{conn: conn} do + user = insert(:user) + limit = Pleroma.Config.get([:instance, :poll_limits, :max_expiration]) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "status" => "imagine arbitrary limits", + "poll" => %{ + "options" => ["this post was made by pleroma gang"], + "expires_in" => limit + 1 + } + }) + + %{"error" => error} = json_response(conn, 422) + assert error == "Expiration date is too far in the future" + end + end + test "posting a sensitive status", %{conn: conn} do user = insert(:user) @@ -317,12 +414,13 @@ test "direct timeline", %{conn: conn} do test "Conversations", %{conn: conn} do user_one = insert(:user) user_two = insert(:user) + user_three = insert(:user) {:ok, user_two} = User.follow(user_two, user_one) {:ok, direct} = CommonAPI.post(user_one, %{ - "status" => "Hi @#{user_two.nickname}!", + "status" => "Hi @#{user_two.nickname}, @#{user_three.nickname}!", "visibility" => "direct" }) @@ -348,7 +446,10 @@ test "Conversations", %{conn: conn} do } ] = response + account_ids = Enum.map(res_accounts, & &1["id"]) assert length(res_accounts) == 2 + assert user_two.id in account_ids + assert user_three.id in account_ids assert is_binary(res_id) assert unread == true assert res_last_status["id"] == direct.id @@ -2538,7 +2639,8 @@ test "get instance information", %{conn: conn} do "stats" => _, "thumbnail" => _, "languages" => _, - "registrations" => _ + "registrations" => _, + "poll_limits" => _ } = result assert email == from_config_email @@ -2852,31 +2954,6 @@ test "unmute conversation", %{conn: conn, user: user, activity: activity} do end end - test "flavours switching (Pleroma Extension)", %{conn: conn} do - user = insert(:user) - - get_old_flavour = - conn - |> assign(:user, user) - |> get("/api/v1/pleroma/flavour") - - assert "glitch" == json_response(get_old_flavour, 200) - - set_flavour = - conn - |> assign(:user, user) - |> post("/api/v1/pleroma/flavour/vanilla") - - assert "vanilla" == json_response(set_flavour, 200) - - get_new_flavour = - conn - |> assign(:user, user) - |> post("/api/v1/pleroma/flavour/vanilla") - - assert json_response(set_flavour, 200) == json_response(get_new_flavour, 200) - end - describe "reports" do setup do reporter = insert(:user) @@ -3462,4 +3539,124 @@ test "rate limit", %{conn: conn} do assert json_response(conn, 403) == %{"error" => "Rate limit exceeded."} end end + + describe "GET /api/v1/polls/:id" do + test "returns poll entity for object id", %{conn: conn} do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "Pleroma does", + "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20} + }) + + object = Object.normalize(activity) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/polls/#{object.id}") + + response = json_response(conn, 200) + id = object.id + assert %{"id" => ^id, "expired" => false, "multiple" => false} = response + end + + test "does not expose polls for private statuses", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "Pleroma does", + "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20}, + "visibility" => "private" + }) + + object = Object.normalize(activity) + + conn = + conn + |> assign(:user, other_user) + |> get("/api/v1/polls/#{object.id}") + + assert json_response(conn, 404) + end + end + + describe "POST /api/v1/polls/:id/votes" do + test "votes are added to the poll", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "A very delicious sandwich", + "poll" => %{ + "options" => ["Lettuce", "Grilled Bacon", "Tomato"], + "expires_in" => 20, + "multiple" => true + } + }) + + object = Object.normalize(activity) + + conn = + conn + |> assign(:user, other_user) + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]}) + + assert json_response(conn, 200) + object = Object.get_by_id(object.id) + + assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} -> + total_items == 1 + end) + end + + test "author can't vote", %{conn: conn} do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "Am I cute?", + "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20} + }) + + object = Object.normalize(activity) + + assert conn + |> assign(:user, user) + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]}) + |> json_response(422) == %{"error" => "Poll's author can't vote"} + + object = Object.get_by_id(object.id) + + refute Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 1 + end + + test "does not allow multiple choices on a single-choice question", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "The glass is", + "poll" => %{"options" => ["half empty", "half full"], "expires_in" => 20} + }) + + object = Object.normalize(activity) + + assert conn + |> assign(:user, other_user) + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]}) + |> json_response(422) == %{"error" => "Too many choices"} + + object = Object.get_by_id(object.id) + + refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => total_items}} -> + total_items == 1 + end) + end + end end diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index d7c800e83..ec75150ab 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -103,6 +103,7 @@ test "a note activity" do muted: false, pinned: false, sensitive: false, + poll: nil, spoiler_text: HtmlSanitizeEx.basic_html(note.data["object"]["summary"]), visibility: "public", media_attachments: [], @@ -341,4 +342,106 @@ test "a rich media card with all relevant data renders correctly" do StatusView.render("card.json", %{page_url: page_url, rich_media: card}) end end + + describe "poll view" do + test "renders a poll" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "Is Tenshi eating a corndog cute?", + "poll" => %{ + "options" => ["absolutely!", "sure", "yes", "why are you even asking?"], + "expires_in" => 20 + } + }) + + object = Object.normalize(activity) + + expected = %{ + emojis: [], + expired: false, + id: object.id, + multiple: false, + options: [ + %{title: "absolutely!", votes_count: 0}, + %{title: "sure", votes_count: 0}, + %{title: "yes", votes_count: 0}, + %{title: "why are you even asking?", votes_count: 0} + ], + voted: false, + votes_count: 0 + } + + result = StatusView.render("poll.json", %{object: object}) + expires_at = result.expires_at + result = Map.delete(result, :expires_at) + + assert result == expected + + expires_at = NaiveDateTime.from_iso8601!(expires_at) + assert NaiveDateTime.diff(expires_at, NaiveDateTime.utc_now()) in 15..20 + end + + test "detects if it is multiple choice" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "Which Mastodon developer is your favourite?", + "poll" => %{ + "options" => ["Gargron", "Eugen"], + "expires_in" => 20, + "multiple" => true + } + }) + + object = Object.normalize(activity) + + assert %{multiple: true} = StatusView.render("poll.json", %{object: object}) + end + + test "detects emoji" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "What's with the smug face?", + "poll" => %{ + "options" => [":blank: sip", ":blank::blank: sip", ":blank::blank::blank: sip"], + "expires_in" => 20 + } + }) + + object = Object.normalize(activity) + + assert %{emojis: [%{shortcode: "blank"}]} = + StatusView.render("poll.json", %{object: object}) + end + + test "detects vote status" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "Which input devices do you use?", + "poll" => %{ + "options" => ["mouse", "trackball", "trackpoint"], + "multiple" => true, + "expires_in" => 20 + } + }) + + object = Object.normalize(activity) + + {:ok, _, object} = CommonAPI.vote(other_user, object, [1, 2]) + + result = StatusView.render("poll.json", %{object: object, for: other_user}) + + assert result[:voted] == true + assert Enum.at(result[:options], 1)[:votes_count] == 1 + assert Enum.at(result[:options], 2)[:votes_count] == 1 + end + end end diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 2cd82b3e7..cab9e5d90 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -102,7 +102,6 @@ test "it updates notification settings", %{conn: conn} do conn |> assign(:user, user) |> put("/api/pleroma/notification_settings", %{ - "remote" => false, "followers" => false, "bar" => 1 }) @@ -110,8 +109,12 @@ test "it updates notification settings", %{conn: conn} do user = Repo.get(User, user.id) - assert %{"remote" => false, "local" => true, "followers" => false, "follows" => true} == - user.info.notification_settings + assert %{ + "followers" => false, + "follows" => true, + "non_follows" => true, + "non_followers" => true + } == user.info.notification_settings end end diff --git a/test/web/twitter_api/views/user_view_test.exs b/test/web/twitter_api/views/user_view_test.exs index 74526673c..a48fc9b78 100644 --- a/test/web/twitter_api/views/user_view_test.exs +++ b/test/web/twitter_api/views/user_view_test.exs @@ -112,9 +112,11 @@ test "User exposes settings for themselves and only for themselves", %{user: use as_user = UserView.render("show.json", %{user: user, for: user}) assert as_user["default_scope"] == user.info.default_scope assert as_user["no_rich_text"] == user.info.no_rich_text + assert as_user["pleroma"]["notification_settings"] == user.info.notification_settings as_stranger = UserView.render("show.json", %{user: user}) refute as_stranger["default_scope"] refute as_stranger["no_rich_text"] + refute as_stranger["pleroma"]["notification_settings"] end test "A user for a given other follower", %{user: user} do