Merging with develop

Conflicts:
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/user.ex
This commit is contained in:
Alex S 2019-05-01 12:29:33 +07:00
commit bdfa3a6fa8
888 changed files with 17070 additions and 68142 deletions

60
lib/healthcheck.ex Normal file
View file

@ -0,0 +1,60 @@
defmodule Pleroma.Healthcheck do
@moduledoc """
Module collects metrics about app and assign healthy status.
"""
alias Pleroma.Healthcheck
alias Pleroma.Repo
defstruct pool_size: 0,
active: 0,
idle: 0,
memory_used: 0,
healthy: true
@type t :: %__MODULE__{
pool_size: non_neg_integer(),
active: non_neg_integer(),
idle: non_neg_integer(),
memory_used: number(),
healthy: boolean()
}
@spec system_info() :: t()
def system_info do
%Healthcheck{
memory_used: Float.round(:erlang.memory(:total) / 1024 / 1024, 2)
}
|> assign_db_info()
|> check_health()
end
defp assign_db_info(healthcheck) do
database = Application.get_env(:pleroma, Repo)[:database]
query =
"select state, count(pid) from pg_stat_activity where datname = '#{database}' group by state;"
result = Repo.query!(query)
pool_size = Application.get_env(:pleroma, Repo)[:pool_size]
db_info =
Enum.reduce(result.rows, %{active: 0, idle: 0}, fn [state, cnt], states ->
if state == "active" do
Map.put(states, :active, states.active + cnt)
else
Map.put(states, :idle, states.idle + cnt)
end
end)
|> Map.put(:pool_size, pool_size)
Map.merge(healthcheck, db_info)
end
@spec check_health(Healthcheck.t()) :: Healthcheck.t()
def check_health(%{pool_size: pool_size, active: active} = check)
when active >= pool_size do
%{check | healthy: false}
end
def check_health(check), do: check
end

View file

@ -0,0 +1,51 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Database do
alias Mix.Tasks.Pleroma.Common
require Logger
use Mix.Task
@shortdoc "A collection of database related tasks"
@moduledoc """
A collection of database related tasks
## Replace embedded objects with their references
Replaces embedded objects with references to them in the `objects` table. Only needs to be ran once. The reason why this is not a migration is because it could significantly increase the database size after being ran, however after this `VACUUM FULL` will be able to reclaim about 20% (really depends on what is in the database, your mileage may vary) of the db size before the migration.
mix pleroma.database remove_embedded_objects
Options:
- `--vacuum` - run `VACUUM FULL` after the embedded objects are replaced with their references
"""
def run(["remove_embedded_objects" | args]) do
{options, [], []} =
OptionParser.parse(
args,
strict: [
vacuum: :boolean
]
)
Common.start_pleroma()
Logger.info("Removing embedded objects")
Pleroma.Repo.query!(
"update activities set data = jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;",
[],
timeout: :infinity
)
if Keyword.get(options, :vacuum) do
Logger.info("Runnning VACUUM FULL")
Pleroma.Repo.query!(
"vacuum full;",
[],
timeout: :infinity
)
end
end
end

View file

@ -0,0 +1,293 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Emoji do
use Mix.Task
@shortdoc "Manages emoji packs"
@moduledoc """
Manages emoji packs
## ls-packs
mix pleroma.emoji ls-packs [OPTION...]
Lists the emoji packs and metadata specified in the manifest.
### Options
- `-m, --manifest PATH/URL` - path to a custom manifest, it can
either be an URL starting with `http`, in that case the
manifest will be fetched from that address, or a local path
## get-packs
mix pleroma.emoji get-packs [OPTION...] PACKS
Fetches, verifies and installs the specified PACKS from the
manifest into the `STATIC-DIR/emoji/PACK-NAME`
### Options
- `-m, --manifest PATH/URL` - same as ls-packs
## gen-pack
mix pleroma.emoji gen-pack PACK-URL
Creates a new manifest entry and a file list from the specified
remote pack file. Currently, only .zip archives are recognized
as remote pack files and packs are therefore assumed to be zip
archives. This command is intended to run interactively and will
first ask you some basic questions about the pack, then download
the remote file and generate an SHA256 checksum for it, then
generate an emoji file list for you.
The manifest entry will either be written to a newly created
`index.json` file or appended to the existing one, *replacing*
the old pack with the same name if it was in the file previously.
The file list will be written to the file specified previously,
*replacing* that file. You _should_ check that the file list doesn't
contain anything you don't need in the pack, that is, anything that is
not an emoji (the whole pack is downloaded, but only emoji files
are extracted).
"""
@default_manifest Pleroma.Config.get!([:emoji, :default_manifest])
def run(["ls-packs" | args]) do
Application.ensure_all_started(:hackney)
{options, [], []} = parse_global_opts(args)
manifest =
fetch_manifest(if options[:manifest], do: options[:manifest], else: @default_manifest)
Enum.each(manifest, fn {name, info} ->
to_print = [
{"Name", name},
{"Homepage", info["homepage"]},
{"Description", info["description"]},
{"License", info["license"]},
{"Source", info["src"]}
]
for {param, value} <- to_print do
IO.puts(IO.ANSI.format([:bright, param, :normal, ": ", value]))
end
# A newline
IO.puts("")
end)
end
def run(["get-packs" | args]) do
Application.ensure_all_started(:hackney)
{options, pack_names, []} = parse_global_opts(args)
manifest_url = if options[:manifest], do: options[:manifest], else: @default_manifest
manifest = fetch_manifest(manifest_url)
for pack_name <- pack_names do
if Map.has_key?(manifest, pack_name) do
pack = manifest[pack_name]
src_url = pack["src"]
IO.puts(
IO.ANSI.format([
"Downloading ",
:bright,
pack_name,
:normal,
" from ",
:underline,
src_url
])
)
binary_archive = Tesla.get!(src_url).body
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright]
if archive_sha == String.upcase(pack["src_sha256"]) do
IO.puts(IO.ANSI.format(sha_status_text ++ [:green, "OK"]))
else
IO.puts(IO.ANSI.format(sha_status_text ++ [:red, "BAD"]))
raise "Bad SHA256 for #{pack_name}"
end
# The url specified in files should be in the same directory
files_url = Path.join(Path.dirname(manifest_url), pack["files"])
IO.puts(
IO.ANSI.format([
"Fetching the file list for ",
:bright,
pack_name,
:normal,
" from ",
:underline,
files_url
])
)
files = Tesla.get!(files_url).body |> Poison.decode!()
IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
pack_path =
Path.join([
Pleroma.Config.get!([:instance, :static_dir]),
"emoji",
pack_name
])
files_to_unzip =
Enum.map(
files,
fn {_, f} -> to_charlist(f) end
)
{:ok, _} =
:zip.unzip(binary_archive,
cwd: pack_path,
file_list: files_to_unzip
)
IO.puts(IO.ANSI.format(["Writing emoji.txt for ", :bright, pack_name]))
emoji_txt_str =
Enum.map(
files,
fn {shortcode, path} ->
emojo_path = Path.join("/emoji/#{pack_name}", path)
"#{shortcode}, #{emojo_path}"
end
)
|> Enum.join("\n")
File.write!(Path.join(pack_path, "emoji.txt"), emoji_txt_str)
else
IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"]))
end
end
end
def run(["gen-pack", src]) do
Application.ensure_all_started(:hackney)
proposed_name = Path.basename(src) |> Path.rootname()
name = String.trim(IO.gets("Pack name [#{proposed_name}]: "))
# If there's no name, use the default one
name = if String.length(name) > 0, do: name, else: proposed_name
license = String.trim(IO.gets("License: "))
homepage = String.trim(IO.gets("Homepage: "))
description = String.trim(IO.gets("Description: "))
proposed_files_name = "#{name}.json"
files_name = String.trim(IO.gets("Save file list to [#{proposed_files_name}]: "))
files_name = if String.length(files_name) > 0, do: files_name, else: proposed_files_name
default_exts = [".png", ".gif"]
default_exts_str = Enum.join(default_exts, " ")
exts =
String.trim(
IO.gets("Emoji file extensions (separated with spaces) [#{default_exts_str}]: ")
)
exts =
if String.length(exts) > 0 do
String.split(exts, " ")
|> Enum.filter(fn e -> e |> String.trim() |> String.length() > 0 end)
else
default_exts
end
IO.puts("Downloading the pack and generating SHA256")
binary_archive = Tesla.get!(src).body
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
IO.puts("SHA256 is #{archive_sha}")
pack_json = %{
name => %{
license: license,
homepage: homepage,
description: description,
src: src,
src_sha256: archive_sha,
files: files_name
}
}
tmp_pack_dir = Path.join(System.tmp_dir!(), "emoji-pack-#{name}")
{:ok, _} =
:zip.unzip(
binary_archive,
cwd: tmp_pack_dir
)
emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts)
File.write!(files_name, Poison.encode!(emoji_map, pretty: true))
IO.puts("""
#{files_name} has been created and contains the list of all found emojis in the pack.
Please review the files in the remove those not needed.
""")
if File.exists?("index.json") do
existing_data = File.read!("index.json") |> Poison.decode!()
File.write!(
"index.json",
Poison.encode!(
Map.merge(
existing_data,
pack_json
),
pretty: true
)
)
IO.puts("index.json file has been update with the #{name} pack")
else
File.write!("index.json", Poison.encode!(pack_json, pretty: true))
IO.puts("index.json has been created with the #{name} pack")
end
end
defp fetch_manifest(from) do
Poison.decode!(
if String.starts_with?(from, "http") do
Tesla.get!(from).body
else
File.read!(from)
end
)
end
defp parse_global_opts(args) do
OptionParser.parse(
args,
strict: [
manifest: :string
],
aliases: [
m: :manifest
]
)
end
end

View file

@ -24,10 +24,12 @@ defmodule Mix.Tasks.Pleroma.Instance do
- `--domain DOMAIN` - the domain of your instance
- `--instance-name INSTANCE_NAME` - the name of your instance
- `--admin-email ADMIN_EMAIL` - the email address of the instance admin
- `--notify-email NOTIFY_EMAIL` - email address for notifications
- `--dbhost HOSTNAME` - the hostname of the PostgreSQL database to use
- `--dbname DBNAME` - the name of the database to use
- `--dbuser DBUSER` - the user (aka role) to use for the database connection
- `--dbpass DBPASS` - the password to use for the database connection
- `--indexable Y/N` - Allow/disallow indexing site by search engines
"""
def run(["gen" | rest]) do
@ -41,10 +43,12 @@ defmodule Mix.Tasks.Pleroma.Instance do
domain: :string,
instance_name: :string,
admin_email: :string,
notify_email: :string,
dbhost: :string,
dbname: :string,
dbuser: :string,
dbpass: :string
dbpass: :string,
indexable: :string
],
aliases: [
o: :output,
@ -61,7 +65,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
will_overwrite = Enum.filter(paths, &File.exists?/1)
proceed? = Enum.empty?(will_overwrite) or Keyword.get(options, :force, false)
unless not proceed? do
if proceed? do
[domain, port | _] =
String.split(
Common.get_option(
@ -81,6 +85,22 @@ defmodule Mix.Tasks.Pleroma.Instance do
email = Common.get_option(options, :admin_email, "What is your admin email address?")
notify_email =
Common.get_option(
options,
:notify_email,
"What email address do you want to use for sending email notifications?",
email
)
indexable =
Common.get_option(
options,
:indexable,
"Do you want search engines to index your site? (y/n)",
"y"
) === "y"
dbhost =
Common.get_option(options, :dbhost, "What is the hostname of your database?", "localhost")
@ -114,6 +134,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
domain: domain,
port: port,
email: email,
notify_email: notify_email,
name: name,
dbhost: dbhost,
dbname: dbname,
@ -142,6 +163,8 @@ defmodule Mix.Tasks.Pleroma.Instance do
Mix.shell().info("Writing #{psql_path}.")
File.write(psql_path, result_psql)
write_robots_txt(indexable)
Mix.shell().info(
"\n" <>
"""
@ -163,4 +186,28 @@ defmodule Mix.Tasks.Pleroma.Instance do
)
end
end
defp write_robots_txt(indexable) do
robots_txt =
EEx.eval_file(
Path.expand("robots_txt.eex", __DIR__),
indexable: indexable
)
static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/")
unless File.exists?(static_dir) do
File.mkdir_p!(static_dir)
end
robots_txt_path = Path.join(static_dir, "robots.txt")
if File.exists?(robots_txt_path) do
File.cp!(robots_txt_path, "#{robots_txt_path}.bak")
Mix.shell().info("Backing up existing robots.txt to #{robots_txt_path}.bak")
end
File.write(robots_txt_path, robots_txt)
Mix.shell().info("Writing #{robots_txt_path}.")
end
end

View file

@ -0,0 +1,2 @@
User-Agent: *
Disallow: <%= if indexable, do: "", else: "/" %>

View file

@ -0,0 +1,32 @@
# Pleroma: A lightweight social networking server
# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.RobotsTxt do
use Mix.Task
@shortdoc "Generate robots.txt"
@moduledoc """
Generates robots.txt
## Overwrite robots.txt to disallow all
mix pleroma.robots_txt disallow_all
This will write a robots.txt that will hide all paths on your instance
from search engines and other robots that obey robots.txt
"""
def run(["disallow_all"]) do
static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/")
if !File.exists?(static_dir) do
File.mkdir_p!(static_dir)
end
robots_txt_path = Path.join(static_dir, "robots.txt")
robots_txt_content = "User-Agent: *\nDisallow: /\n"
File.write!(robots_txt_path, robots_txt_content, [:write])
end
end

View file

@ -13,6 +13,7 @@ config :pleroma, Pleroma.Web.Endpoint,
config :pleroma, :instance,
name: "<%= name %>",
email: "<%= email %>",
notify_email: "<%= notify_email %>",
limit: 5000,
registrations_open: true,
dedupe_media: false
@ -75,4 +76,3 @@ config :web_push_encryption, :vapid_details,
# storage_url: "https://swift-endpoint.prodider.com/v1/AUTH_<tenant>/<container>",
# object_url: "https://cdn-endpoint.provider.com/<container>"
#

View file

@ -6,8 +6,8 @@ defmodule Mix.Tasks.Pleroma.User do
use Mix.Task
import Ecto.Changeset
alias Mix.Tasks.Pleroma.Common
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserInviteToken
@shortdoc "Manages Pleroma users"
@moduledoc """
@ -23,16 +23,32 @@ defmodule Mix.Tasks.Pleroma.User do
- `--password PASSWORD` - the user's password
- `--moderator`/`--no-moderator` - whether the user is a moderator
- `--admin`/`--no-admin` - whether the user is an admin
- `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions
- `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions
## Generate an invite link.
mix pleroma.user invite
mix pleroma.user invite [OPTION...]
Options:
- `--expires_at DATE` - last day on which token is active (e.g. "2019-04-05")
- `--max_use NUMBER` - maximum numbers of token uses
## List generated invites
mix pleroma.user invites
## Revoke invite
mix pleroma.user revoke_invite TOKEN OR TOKEN_ID
## Delete the user's account.
mix pleroma.user rm NICKNAME
## Delete the user's activities.
mix pleroma.user delete_activities NICKNAME
## Deactivate or activate the user's account.
mix pleroma.user toggle_activated NICKNAME
@ -146,7 +162,7 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["rm", nickname]) do
Common.start_pleroma()
with %User{local: true} = user <- User.get_by_nickname(nickname) do
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.delete(user)
Mix.shell().info("User #{nickname} deleted.")
else
@ -158,7 +174,7 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["toggle_activated", nickname]) do
Common.start_pleroma()
with %User{} = user <- User.get_by_nickname(nickname) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} = User.deactivate(user, !user.info.deactivated)
Mix.shell().info(
@ -173,7 +189,7 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["reset_password", nickname]) do
Common.start_pleroma()
with %User{local: true} = user <- User.get_by_nickname(nickname),
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
Mix.shell().info("Generated password reset token for #{user.nickname}")
@ -195,14 +211,14 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["unsubscribe", nickname]) do
Common.start_pleroma()
with %User{} = user <- User.get_by_nickname(nickname) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
Mix.shell().info("Deactivating #{user.nickname}")
User.deactivate(user)
{:ok, friends} = User.get_friends(user)
Enum.each(friends, fn friend ->
user = Repo.get(User, user.id)
user = User.get_cached_by_id(user.id)
Mix.shell().info("Unsubscribing #{friend.nickname} from #{user.nickname}")
User.unfollow(user, friend)
@ -210,7 +226,7 @@ defmodule Mix.Tasks.Pleroma.User do
:timer.sleep(500)
user = Repo.get(User, user.id)
user = User.get_cached_by_id(user.id)
if Enum.empty?(user.following) do
Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}")
@ -234,7 +250,7 @@ defmodule Mix.Tasks.Pleroma.User do
]
)
with %User{local: true} = user <- User.get_by_nickname(nickname) do
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
user =
case Keyword.get(options, :moderator) do
nil -> user
@ -261,7 +277,7 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["tag", nickname | tags]) do
Common.start_pleroma()
with %User{} = user <- User.get_by_nickname(nickname) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.tag(tags)
Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}")
@ -274,7 +290,7 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["untag", nickname | tags]) do
Common.start_pleroma()
with %User{} = user <- User.get_by_nickname(nickname) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.untag(tags)
Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}")
@ -284,23 +300,91 @@ defmodule Mix.Tasks.Pleroma.User do
end
end
def run(["invite"]) do
def run(["invite" | rest]) do
{options, [], []} =
OptionParser.parse(rest,
strict: [
expires_at: :string,
max_use: :integer
]
)
options =
options
|> Keyword.update(:expires_at, {:ok, nil}, fn
nil -> {:ok, nil}
val -> Date.from_iso8601(val)
end)
|> Enum.into(%{})
Common.start_pleroma()
with {:ok, token} <- Pleroma.UserInviteToken.create_token() do
Mix.shell().info("Generated user invite token")
with {:ok, val} <- options[:expires_at],
options = Map.put(options, :expires_at, val),
{:ok, invite} <- UserInviteToken.create_invite(options) do
Mix.shell().info(
"Generated user invite token " <> String.replace(invite.invite_type, "_", " ")
)
url =
Pleroma.Web.Router.Helpers.redirect_url(
Pleroma.Web.Endpoint,
:registration_page,
token.token
invite.token
)
IO.puts(url)
else
error ->
Mix.shell().error("Could not create invite token: #{inspect(error)}")
end
end
def run(["invites"]) do
Common.start_pleroma()
Mix.shell().info("Invites list:")
UserInviteToken.list_invites()
|> Enum.each(fn invite ->
expire_info =
with expires_at when not is_nil(expires_at) <- invite.expires_at do
" | Expires at: #{Date.to_string(expires_at)}"
end
using_info =
with max_use when not is_nil(max_use) <- invite.max_use do
" | Max use: #{max_use} Left use: #{max_use - invite.uses}"
end
Mix.shell().info(
"ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{
invite.used
}#{expire_info}#{using_info}"
)
end)
end
def run(["revoke_invite", token]) do
Common.start_pleroma()
with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, _} <- UserInviteToken.update_invite(invite, %{used: true}) do
Mix.shell().info("Invite for token #{token} was revoked.")
else
_ -> Mix.shell().error("No invite found with token #{token}")
end
end
def run(["delete_activities", nickname]) do
Common.start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.delete_user_activities(user)
Mix.shell().info("User #{nickname} statuses deleted.")
else
_ ->
Mix.shell().error("Could not create invite token.")
Mix.shell().error("No local user #{nickname}")
end
end

View file

@ -39,7 +39,7 @@ defmodule Pleroma.PasswordResetToken do
def reset_password(token, data) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
%User{} = user <- Repo.get(User, token.user_id),
%User{} = user <- User.get_cached_by_id(token.user_id),
{:ok, _user} <- User.reset_password(user, data),
{:ok, token} <- Repo.update(used_changeset(token)) do
{:ok, token}

View file

@ -7,8 +7,10 @@ defmodule Pleroma.Activity do
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
import Ecto.Changeset
import Ecto.Query
@type t :: %__MODULE__{}
@ -22,16 +24,53 @@ defmodule Pleroma.Activity do
"Like" => "favourite"
}
@mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
into: %{},
do: {v, k}
schema "activities" do
field(:data, :map)
field(:local, :boolean, default: true)
field(:actor, :string)
field(:recipients, {:array, :string})
field(:recipients, {:array, :string}, default: [])
has_many(:notifications, Notification, on_delete: :delete_all)
# Attention: this is a fake relation, don't try to preload it blindly and expect it to work!
# The foreign key is embedded in a jsonb field.
#
# To use it, you probably want to do an inner join and a preload:
#
# ```
# |> join(:inner, [activity], o in Object,
# on: fragment("(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')",
# o.data, activity.data, activity.data))
# |> preload([activity, object], [object: object])
# ```
#
# As a convenience, Activity.with_preloaded_object() sets up an inner join and preload for the
# typical case.
has_one(:object, Object, on_delete: :nothing, foreign_key: :id)
timestamps()
end
def with_preloaded_object(query) do
query
|> join(
:inner,
[activity],
o in Object,
on:
fragment(
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
o.data,
activity.data,
activity.data
)
)
|> preload([activity, object], object: object)
end
def get_by_ap_id(ap_id) do
Repo.one(
from(
@ -41,10 +80,51 @@ defmodule Pleroma.Activity do
)
end
def change(struct, params \\ %{}) do
struct
|> cast(params, [:data])
|> validate_required([:data])
|> unique_constraint(:ap_id, name: :activities_unique_apid_index)
end
def get_by_ap_id_with_object(ap_id) do
Repo.one(
from(
activity in Activity,
where: fragment("(?)->>'id' = ?", activity.data, ^to_string(ap_id)),
left_join: o in Object,
on:
fragment(
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
o.data,
activity.data,
activity.data
),
preload: [object: o]
)
)
end
def get_by_id(id) do
Repo.get(Activity, id)
end
def get_by_id_with_object(id) do
from(activity in Activity,
where: activity.id == ^id,
inner_join: o in Object,
on:
fragment(
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
o.data,
activity.data,
activity.data
),
preload: [object: o]
)
|> Repo.one()
end
def by_object_ap_id(ap_id) do
from(
activity in Activity,
@ -72,7 +152,7 @@ defmodule Pleroma.Activity do
)
end
def create_by_object_ap_id(ap_id) do
def create_by_object_ap_id(ap_id) when is_binary(ap_id) do
from(
activity in Activity,
where:
@ -86,6 +166,8 @@ defmodule Pleroma.Activity do
)
end
def create_by_object_ap_id(_), do: nil
def get_all_create_by_object_ap_id(ap_id) do
Repo.all(create_by_object_ap_id(ap_id))
end
@ -97,21 +179,60 @@ defmodule Pleroma.Activity do
def get_create_by_object_ap_id(_), do: nil
def normalize(obj) when is_map(obj), do: Activity.get_by_ap_id(obj["id"])
def normalize(ap_id) when is_binary(ap_id), do: Activity.get_by_ap_id(ap_id)
def normalize(_), do: nil
def get_in_reply_to_activity(%Activity{data: %{"object" => %{"inReplyTo" => ap_id}}}) do
get_create_by_object_ap_id(ap_id)
def create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
from(
activity in Activity,
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^to_string(ap_id)
),
where: fragment("(?)->>'type' = 'Create'", activity.data),
inner_join: o in Object,
on:
fragment(
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
o.data,
activity.data,
activity.data
),
preload: [object: o]
)
end
def get_in_reply_to_activity(_), do: nil
def create_by_object_ap_id_with_object(_), do: nil
def get_create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
ap_id
|> create_by_object_ap_id_with_object()
|> Repo.one()
end
def get_create_by_object_ap_id_with_object(_), do: nil
defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do
get_create_by_object_ap_id_with_object(ap_id)
end
defp get_in_reply_to_activity_from_object(_), do: nil
def get_in_reply_to_activity(%Activity{data: %{"object" => object}}) do
get_in_reply_to_activity_from_object(Object.normalize(object))
end
def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
def normalize(_), do: nil
def delete_by_ap_id(id) when is_binary(id) do
by_object_ap_id(id)
|> Repo.delete_all(returning: true)
|> select([u], u)
|> Repo.delete_all()
|> elem(1)
|> Enum.find(fn
%{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id
%{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
_ -> nil
end)
@ -126,6 +247,10 @@ defmodule Pleroma.Activity do
def mastodon_notification_type(%Activity{}), do: nil
def from_mastodon_notification_type(type) do
Map.get(@mastodon_to_ap_notification_types, type)
end
def all_by_actor_and_id(actor, status_ids \\ [])
def all_by_actor_and_id(_actor, []), do: []

View file

@ -25,6 +25,7 @@ defmodule Pleroma.Application do
import Cachex.Spec
Pleroma.Config.DeprecationWarnings.warn()
setup_instrumenters()
# Define workers and child supervisors to be supervised
children =
@ -103,15 +104,15 @@ defmodule Pleroma.Application do
],
id: :cachex_idem
),
worker(Pleroma.FlakeId, [])
worker(Pleroma.FlakeId, []),
worker(Pleroma.ScheduledActivityWorker, [])
] ++
hackney_pool_children() ++
[
worker(Pleroma.Web.Federator.RetryQueue, []),
worker(Pleroma.Stats, []),
worker(Pleroma.Web.Push, []),
worker(Pleroma.Jobs, []),
worker(Task, [&Pleroma.Web.Federator.init/0], restart: :temporary)
worker(Task, [&Pleroma.Web.Push.init/0], restart: :temporary, id: :web_push_init),
worker(Task, [&Pleroma.Web.Federator.init/0], restart: :temporary, id: :federator_init)
] ++
streamer_child() ++
chat_child() ++
@ -127,6 +128,24 @@ defmodule Pleroma.Application do
Supervisor.start_link(children, opts)
end
defp setup_instrumenters do
require Prometheus.Registry
:ok =
:telemetry.attach(
"prometheus-ecto",
[:pleroma, :repo, :query],
&Pleroma.Repo.Instrumenter.handle_event/4,
%{}
)
Prometheus.Registry.register_collector(:prometheus_process_collector)
Pleroma.Web.Endpoint.MetricsExporter.setup()
Pleroma.Web.Endpoint.PipelineInstrumenter.setup()
Pleroma.Web.Endpoint.Instrumenter.setup()
Pleroma.Repo.Instrumenter.setup()
end
def enabled_hackney_pools do
[:media] ++
if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do

60
lib/pleroma/bookmark.ex Normal file
View file

@ -0,0 +1,60 @@
defmodule Pleroma.Bookmark do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.FlakeId
alias Pleroma.Repo
alias Pleroma.User
@type t :: %__MODULE__{}
schema "bookmarks" do
belongs_to(:user, User, type: FlakeId)
belongs_to(:activity, Activity, type: FlakeId)
timestamps()
end
@spec create(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()}
def create(user_id, activity_id) do
attrs = %{
user_id: user_id,
activity_id: activity_id
}
%Bookmark{}
|> cast(attrs, [:user_id, :activity_id])
|> validate_required([:user_id, :activity_id])
|> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index)
|> Repo.insert()
end
@spec for_user_query(FlakeId.t()) :: Ecto.Query.t()
def for_user_query(user_id) do
Bookmark
|> where(user_id: ^user_id)
|> join(:inner, [b], activity in assoc(b, :activity))
|> preload([b, a], activity: a)
end
def get(user_id, activity_id) do
Bookmark
|> where(user_id: ^user_id)
|> where(activity_id: ^activity_id)
|> Repo.one()
end
@spec destroy(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()}
def destroy(user_id, activity_id) do
from(b in Bookmark,
where: b.user_id == ^user_id,
where: b.activity_id == ^activity_id
)
|> Repo.one()
|> Repo.delete()
end
end

View file

@ -57,4 +57,8 @@ defmodule Pleroma.Config do
def delete(key) do
Application.delete_env(:pleroma, key)
end
def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], [])
def oauth_consumer_enabled?, do: oauth_consumer_strategies() != []
end

View file

@ -2,7 +2,7 @@
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.AdminEmail do
defmodule Pleroma.Emails.AdminEmail do
@moduledoc "Admin emails"
import Swoosh.Email
@ -11,7 +11,10 @@ defmodule Pleroma.AdminEmail do
defp instance_config, do: Pleroma.Config.get(:instance)
defp instance_name, do: instance_config()[:name]
defp instance_email, do: instance_config()[:email]
defp instance_notify_email do
Keyword.get(instance_config(), :notify_email, instance_config()[:email])
end
defp user_url(user) do
Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, user.nickname)
@ -29,9 +32,13 @@ defmodule Pleroma.AdminEmail do
if length(statuses) > 0 do
statuses_list_html =
statuses
|> Enum.map(fn %{id: id} ->
status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, id)
"<li><a href=\"#{status_url}\">#{status_url}</li>"
|> Enum.map(fn
%{id: id} ->
status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, id)
"<li><a href=\"#{status_url}\">#{status_url}</li>"
id when is_binary(id) ->
"<li><a href=\"#{id}\">#{id}</li>"
end)
|> Enum.join("\n")
@ -55,7 +62,7 @@ defmodule Pleroma.AdminEmail do
new()
|> to({to.name, to.email})
|> from({instance_name(), instance_email()})
|> from({instance_name(), instance_notify_email()})
|> reply_to({reporter.name, reporter.email})
|> subject("#{instance_name()} Report")
|> html_body(html_body)

View file

@ -2,11 +2,11 @@
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Mailer do
defmodule Pleroma.Emails.Mailer do
use Swoosh.Mailer, otp_app: :pleroma
def deliver_async(email, config \\ []) do
Pleroma.Jobs.enqueue(:mailer, __MODULE__, [:deliver_async, email, config])
PleromaJobQueue.enqueue(:mailer, __MODULE__, [:deliver_async, email, config])
end
def perform(:deliver_async, email, config), do: deliver(email, config)

View file

@ -2,7 +2,7 @@
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserEmail do
defmodule Pleroma.Emails.UserEmail do
@moduledoc "User emails"
import Swoosh.Email
@ -15,7 +15,8 @@ defmodule Pleroma.UserEmail do
defp instance_name, do: instance_config()[:name]
defp sender do
{instance_name(), instance_config()[:email]}
email = Keyword.get(instance_config(), :notify_email, instance_config()[:email])
{instance_name(), email}
end
defp recipient(email, nil), do: email

View file

@ -6,15 +6,23 @@ defmodule Pleroma.Emoji do
@moduledoc """
The emojis are loaded from:
* the built-in Finmojis (if enabled in configuration),
* emoji packs in INSTANCE-DIR/emoji
* the files: `config/emoji.txt` and `config/custom_emoji.txt`
* glob paths
* glob paths, nested folder is used as tag name for grouping e.g. priv/static/emoji/custom/nested_folder
This GenServer stores in an ETS table the list of the loaded emojis, and also allows to reload the list at runtime.
"""
use GenServer
require Logger
@type pattern :: Regex.t() | module() | String.t()
@type patterns :: pattern() | [pattern()]
@type group_patterns :: keyword(patterns())
@ets __MODULE__.Ets
@ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}]
@groups Application.get_env(:pleroma, :emoji)[:groups]
@doc false
def start_link do
@ -73,91 +81,94 @@ defmodule Pleroma.Emoji do
end
defp load do
emoji_dir_path =
Path.join(
Pleroma.Config.get!([:instance, :static_dir]),
"emoji"
)
case File.ls(emoji_dir_path) do
{:error, :enoent} ->
# The custom emoji directory doesn't exist,
# don't do anything
nil
{:error, e} ->
# There was some other error
Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}")
{:ok, packs} ->
# Print the packs we've found
Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")
emojis =
Enum.flat_map(
packs,
fn pack -> load_pack(Path.join(emoji_dir_path, pack)) end
)
true = :ets.insert(@ets, emojis)
end
# Compat thing for old custom emoji handling & default emoji,
# it should run even if there are no emoji packs
shortcode_globs = Application.get_env(:pleroma, :emoji)[:shortcode_globs] || []
emojis =
(load_finmoji(Keyword.get(Application.get_env(:pleroma, :instance), :finmoji_enabled)) ++
load_from_file("config/emoji.txt") ++
(load_from_file("config/emoji.txt") ++
load_from_file("config/custom_emoji.txt") ++
load_from_globs(
Keyword.get(Application.get_env(:pleroma, :emoji, []), :shortcode_globs, [])
))
load_from_globs(shortcode_globs))
|> Enum.reject(fn value -> value == nil end)
true = :ets.insert(@ets, emojis)
:ok
end
@finmoji [
"a_trusted_friend",
"alandislands",
"association",
"auroraborealis",
"baby_in_a_box",
"bear",
"black_gold",
"christmasparty",
"crosscountryskiing",
"cupofcoffee",
"education",
"fashionista_finns",
"finnishlove",
"flag",
"forest",
"four_seasons_of_bbq",
"girlpower",
"handshake",
"happiness",
"headbanger",
"icebreaker",
"iceman",
"joulutorttu",
"kaamos",
"kalsarikannit_f",
"kalsarikannit_m",
"karjalanpiirakka",
"kicksled",
"kokko",
"lavatanssit",
"losthopes_f",
"losthopes_m",
"mattinykanen",
"meanwhileinfinland",
"moominmamma",
"nordicfamily",
"out_of_office",
"peacemaker",
"perkele",
"pesapallo",
"polarbear",
"pusa_hispida_saimensis",
"reindeer",
"sami",
"sauna_f",
"sauna_m",
"sauna_whisk",
"sisu",
"stuck",
"suomimainittu",
"superfood",
"swan",
"the_cap",
"the_conductor",
"the_king",
"the_voice",
"theoriginalsanta",
"tomoffinland",
"torillatavataan",
"unbreakable",
"waiting",
"white_nights",
"woollysocks"
]
defp load_finmoji(true) do
Enum.map(@finmoji, fn finmoji ->
{finmoji, "/finmoji/128px/#{finmoji}-128.png"}
end)
defp load_pack(pack_dir) do
pack_name = Path.basename(pack_dir)
emoji_txt = Path.join(pack_dir, "emoji.txt")
if File.exists?(emoji_txt) do
load_from_file(emoji_txt)
else
Logger.info(
"No emoji.txt found for pack \"#{pack_name}\", assuming all .png files are emoji"
)
make_shortcode_to_file_map(pack_dir, [".png"])
|> Enum.map(fn {shortcode, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file)
{shortcode, filename, [to_string(match_extra(@groups, filename))]}
end)
end
end
defp load_finmoji(_), do: []
def make_shortcode_to_file_map(pack_dir, exts) do
find_all_emoji(pack_dir, exts)
|> Enum.map(&Path.relative_to(&1, pack_dir))
|> Enum.map(fn f -> {f |> Path.basename() |> Path.rootname(), f} end)
|> Enum.into(%{})
end
def find_all_emoji(dir, exts) do
Enum.reduce(
File.ls!(dir),
[],
fn f, acc ->
filepath = Path.join(dir, f)
if File.dir?(filepath) do
acc ++ find_all_emoji(filepath, exts)
else
acc ++ [filepath]
end
end
)
|> Enum.filter(fn f -> Path.extname(f) in exts end)
end
defp load_from_file(file) do
if File.exists?(file) do
@ -172,8 +183,14 @@ defmodule Pleroma.Emoji do
|> Stream.map(&String.trim/1)
|> Stream.map(fn line ->
case String.split(line, ~r/,\s*/) do
[name, file] -> {name, file}
_ -> nil
[name, file] ->
{name, file, [to_string(match_extra(@groups, file))]}
[name, file | tags] ->
{name, file, tags}
_ ->
nil
end
end)
|> Enum.to_list()
@ -190,9 +207,40 @@ defmodule Pleroma.Emoji do
|> Enum.concat()
Enum.map(paths, fn path ->
tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path)))
shortcode = Path.basename(path, Path.extname(path))
external_path = Path.join("/", Path.relative_to(path, static_path))
{shortcode, external_path}
{shortcode, external_path, [to_string(tag)]}
end)
end
@doc """
Finds a matching group for the given emoji filename
"""
@spec match_extra(group_patterns(), String.t()) :: atom() | nil
def match_extra(group_patterns, filename) do
match_group_patterns(group_patterns, fn pattern ->
case pattern do
%Regex{} = regex -> Regex.match?(regex, filename)
string when is_binary(string) -> filename == string
end
end)
end
defp match_group_patterns(group_patterns, matcher) do
Enum.find_value(group_patterns, fn {group, patterns} ->
patterns =
patterns
|> List.wrap()
|> Enum.map(fn pattern ->
if String.contains?(pattern, "*") do
~r(#{String.replace(pattern, "*", ".*")})
else
pattern
end
end)
Enum.any?(patterns, matcher) && group
end)
end
end

View file

@ -46,7 +46,7 @@ defmodule Pleroma.FlakeId do
def from_string(string) when is_binary(string) and byte_size(string) < 18 do
case Integer.parse(string) do
{id, _} -> <<0::integer-size(64), id::integer-size(64)>>
{id, ""} -> <<0::integer-size(64), id::integer-size(64)>>
_ -> nil
end
end

View file

@ -8,20 +8,32 @@ defmodule Pleroma.Formatter do
alias Pleroma.User
alias Pleroma.Web.MediaProxy
@safe_mention_regex ~r/^(\s*(?<mentions>@.+?\s+)+)(?<rest>.*)/
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
@link_regex ~r{((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+}ui
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
@auto_linker_config hashtag: true,
hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
mention: true,
mention_handler: &Pleroma.Formatter.mention_handler/4
def escape_mention_handler("@" <> nickname = mention, buffer, _, _) do
case User.get_cached_by_nickname(nickname) do
%User{} ->
# escape markdown characters with `\\`
# (we don't want something like @user__name to be parsed by markdown)
String.replace(mention, @markdown_characters_regex, "\\\\\\1")
_ ->
buffer
end
end
def mention_handler("@" <> nickname, buffer, opts, acc) do
case User.get_cached_by_nickname(nickname) do
%User{id: id} = user ->
ap_id = get_ap_id(user)
nickname_text = get_nickname_text(nickname, opts) |> maybe_escape(opts)
nickname_text = get_nickname_text(nickname, opts)
link =
"<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{ap_id}'>@<span>#{
@ -45,15 +57,47 @@ defmodule Pleroma.Formatter do
@doc """
Parses a text and replace plain text links with HTML. Returns a tuple with a result text, mentions, and hashtags.
If the 'safe_mention' option is given, only consecutive mentions at the start the post are actually mentioned.
"""
@spec linkify(String.t(), keyword()) ::
{String.t(), [{String.t(), User.t()}], [{String.t(), String.t()}]}
def linkify(text, options \\ []) do
options = options ++ @auto_linker_config
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
{text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)
{text, MapSet.to_list(mentions), MapSet.to_list(tags)}
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
{text_mentions, %{mentions: mentions}} = AutoLinker.link_map(mentions, acc, options)
{text_rest, %{tags: tags}} = AutoLinker.link_map(rest, acc, options)
{text_mentions <> text_rest, MapSet.to_list(mentions), MapSet.to_list(tags)}
else
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
{text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)
{text, MapSet.to_list(mentions), MapSet.to_list(tags)}
end
end
@doc """
Escapes a special characters in mention names.
"""
def mentions_escape(text, options \\ []) do
options =
Keyword.merge(options,
mention: true,
url: false,
mention_handler: &Pleroma.Formatter.escape_mention_handler/4
)
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
AutoLinker.link(mentions, options) <> AutoLinker.link(rest, options)
else
AutoLinker.link(text, options)
end
end
def emojify(text) do
@ -63,9 +107,9 @@ defmodule Pleroma.Formatter do
def emojify(text, nil), do: text
def emojify(text, emoji, strip \\ false) do
Enum.reduce(emoji, text, fn {emoji, file}, text ->
emoji = HTML.strip_tags(emoji)
file = HTML.strip_tags(file)
Enum.reduce(emoji, text, fn emoji_data, text ->
emoji = HTML.strip_tags(elem(emoji_data, 0))
file = HTML.strip_tags(elem(emoji_data, 1))
html =
if not strip do
@ -87,7 +131,7 @@ defmodule Pleroma.Formatter do
def demojify(text, nil), do: text
def get_emoji(text) when is_binary(text) do
Enum.filter(Emoji.get_all(), fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end)
Enum.filter(Emoji.get_all(), fn {emoji, _, _} -> String.contains?(text, ":#{emoji}:") end)
end
def get_emoji(_), do: []
@ -126,10 +170,4 @@ defmodule Pleroma.Formatter do
defp get_nickname_text(nickname, %{mentions_format: :full}), do: User.full_nickname(nickname)
defp get_nickname_text(nickname, _), do: User.local_nickname(nickname)
defp maybe_escape(str, %{mentions_escape: true}) do
String.replace(str, @markdown_characters_regex, "\\\\\\1")
end
defp maybe_escape(str, _), do: str
end

View file

@ -38,7 +38,7 @@ end
defmodule Pleroma.Gopher.Server.ProtocolHandler do
alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.Repo
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
@ -66,7 +66,8 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
def link(name, selector, type \\ 1) do
address = Pleroma.Web.Endpoint.host()
port = Pleroma.Config.get([:gopher, :port], 1234)
"#{type}#{name}\t#{selector}\t#{address}\t#{port}\r\n"
dstport = Pleroma.Config.get([:gopher, :dstport], port)
"#{type}#{name}\t#{selector}\t#{address}\t#{dstport}\r\n"
end
def render_activities(activities) do
@ -75,14 +76,14 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
|> Enum.map(fn activity ->
user = User.get_cached_by_ap_id(activity.data["actor"])
object = activity.data["object"]
object = Object.normalize(activity)
like_count = object["like_count"] || 0
announcement_count = object["announcement_count"] || 0
link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <>
info("#{like_count} likes, #{announcement_count} repeats") <>
"i\tfake\t(NULL)\t0\r\n" <>
info(HTML.strip_tags(String.replace(activity.data["object"]["content"], "<br>", "\r")))
info(HTML.strip_tags(String.replace(object["content"], "<br>", "\r")))
end)
|> Enum.join("i\tfake\t(NULL)\t0\r\n")
end
@ -110,7 +111,7 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
end
def response("/notices/" <> id) do
with %Activity{} = activity <- Repo.get(Activity, id),
with %Activity{} = activity <- Activity.get_by_id(id),
true <- Visibility.is_public?(activity) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"])

View file

@ -28,27 +28,40 @@ defmodule Pleroma.HTML do
def filter_tags(html), do: filter_tags(html, nil)
def strip_tags(html), do: Scrubber.scrub(html, Scrubber.StripTags)
def get_cached_scrubbed_html_for_object(content, scrubbers, object, module) do
key = "#{module}#{generate_scrubber_signature(scrubbers)}|#{object.id}"
Cachex.fetch!(:scrubber_cache, key, fn _key -> ensure_scrubbed_html(content, scrubbers) end)
def get_cached_scrubbed_html_for_activity(content, scrubbers, activity, key \\ "") do
key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}"
Cachex.fetch!(:scrubber_cache, key, fn _key ->
object = Pleroma.Object.normalize(activity)
ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false)
end)
end
def get_cached_stripped_html_for_object(content, object, module) do
get_cached_scrubbed_html_for_object(
def get_cached_stripped_html_for_activity(content, activity, key) do
get_cached_scrubbed_html_for_activity(
content,
HtmlSanitizeEx.Scrubber.StripTags,
object,
module
activity,
key
)
end
def ensure_scrubbed_html(
content,
scrubbers
scrubbers,
false = _fake
) do
{:commit, filter_tags(content, scrubbers)}
end
def ensure_scrubbed_html(
content,
scrubbers,
true = _fake
) do
{:ignore, filter_tags(content, scrubbers)}
end
defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do
generate_scrubber_signature([scrubber])
end
@ -93,14 +106,31 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
# links
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])
Meta.allow_tag_with_this_attribute_values("a", "class", [
"hashtag",
"u-url",
"mention",
"u-url mention",
"mention u-url"
])
Meta.allow_tag_with_this_attribute_values("a", "rel", [
"tag",
"nofollow",
"noopener",
"noreferrer"
])
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
# paragraphs and linebreaks
Meta.allow_tag_with_these_attributes("br", [])
Meta.allow_tag_with_these_attributes("p", [])
# microformats
Meta.allow_tag_with_these_attributes("span", ["class"])
Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"])
Meta.allow_tag_with_these_attributes("span", [])
# allow inline images for custom emoji
@allow_inline_images Keyword.get(@markup, :allow_inline_images)
@ -135,7 +165,23 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.strip_comments()
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])
Meta.allow_tag_with_this_attribute_values("a", "class", [
"hashtag",
"u-url",
"mention",
"u-url mention",
"mention u-url"
])
Meta.allow_tag_with_this_attribute_values("a", "rel", [
"tag",
"nofollow",
"noopener",
"noreferrer"
])
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
Meta.allow_tag_with_these_attributes("abbr", ["title"])
@ -150,11 +196,13 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("ol", [])
Meta.allow_tag_with_these_attributes("p", [])
Meta.allow_tag_with_these_attributes("pre", [])
Meta.allow_tag_with_these_attributes("span", ["class"])
Meta.allow_tag_with_these_attributes("strong", [])
Meta.allow_tag_with_these_attributes("u", [])
Meta.allow_tag_with_these_attributes("ul", [])
Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"])
Meta.allow_tag_with_these_attributes("span", [])
@allow_inline_images Keyword.get(@markup, :allow_inline_images)
if @allow_inline_images do

View file

@ -12,7 +12,7 @@ defmodule Pleroma.Instances.Instance do
schema "instances" do
field(:host, :string)
field(:unreachable_since, :naive_datetime)
field(:unreachable_since, :naive_datetime_usec)
timestamps()
end

View file

@ -1,152 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Jobs do
@moduledoc """
A basic job queue
"""
use GenServer
require Logger
def init(args) do
{:ok, args}
end
def start_link do
queues =
Pleroma.Config.get(Pleroma.Jobs)
|> Enum.map(fn {name, _} -> create_queue(name) end)
|> Enum.into(%{})
state = %{
queues: queues,
refs: %{}
}
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def create_queue(name) do
{name, {:sets.new(), []}}
end
@doc """
Enqueues a job.
Returns `:ok`.
## Arguments
- `queue_name` - a queue name(must be specified in the config).
- `mod` - a worker module (must have `perform` function).
- `args` - a list of arguments for the `perform` function of the worker module.
- `priority` - a job priority (`0` by default).
## Examples
Enqueue `Module.perform/0` with `priority=1`:
iex> Pleroma.Jobs.enqueue(:example_queue, Module, [])
:ok
Enqueue `Module.perform(:job_name)` with `priority=5`:
iex> Pleroma.Jobs.enqueue(:example_queue, Module, [:job_name], 5)
:ok
Enqueue `Module.perform(:another_job, data)` with `priority=1`:
iex> data = "foobar"
iex> Pleroma.Jobs.enqueue(:example_queue, Module, [:another_job, data])
:ok
Enqueue `Module.perform(:foobar_job, :foo, :bar, 42)` with `priority=1`:
iex> Pleroma.Jobs.enqueue(:example_queue, Module, [:foobar_job, :foo, :bar, 42])
:ok
"""
def enqueue(queue_name, mod, args, priority \\ 1)
if Mix.env() == :test do
def enqueue(_queue_name, mod, args, _priority) do
apply(mod, :perform, args)
end
else
@spec enqueue(atom(), atom(), [any()], integer()) :: :ok
def enqueue(queue_name, mod, args, priority) do
GenServer.cast(__MODULE__, {:enqueue, queue_name, mod, args, priority})
end
end
def handle_cast({:enqueue, queue_name, mod, args, priority}, state) do
{running_jobs, queue} = state[:queues][queue_name]
queue = enqueue_sorted(queue, {mod, args}, priority)
state =
state
|> update_queue(queue_name, {running_jobs, queue})
|> maybe_start_job(queue_name, running_jobs, queue)
{:noreply, state}
end
def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
queue_name = state.refs[ref]
{running_jobs, queue} = state[:queues][queue_name]
running_jobs = :sets.del_element(ref, running_jobs)
state =
state
|> remove_ref(ref)
|> update_queue(queue_name, {running_jobs, queue})
|> maybe_start_job(queue_name, running_jobs, queue)
{:noreply, state}
end
def maybe_start_job(state, queue_name, running_jobs, queue) do
if :sets.size(running_jobs) < Pleroma.Config.get([__MODULE__, queue_name, :max_jobs]) &&
queue != [] do
{{mod, args}, queue} = queue_pop(queue)
{:ok, pid} = Task.start(fn -> apply(mod, :perform, args) end)
mref = Process.monitor(pid)
state
|> add_ref(queue_name, mref)
|> update_queue(queue_name, {:sets.add_element(mref, running_jobs), queue})
else
state
end
end
def enqueue_sorted(queue, element, priority) do
[%{item: element, priority: priority} | queue]
|> Enum.sort_by(fn %{priority: priority} -> priority end)
end
def queue_pop([%{item: element} | queue]) do
{element, queue}
end
defp add_ref(state, queue_name, ref) do
refs = Map.put(state[:refs], ref, queue_name)
Map.put(state, :refs, refs)
end
defp remove_ref(state, ref) do
refs = Map.delete(state[:refs], ref)
Map.put(state, :refs, refs)
end
defp update_queue(state, queue_name, data) do
queues = Map.put(state[:queues], queue_name, data)
Map.put(state, :queues, queues)
end
end

View file

@ -80,7 +80,7 @@ defmodule Pleroma.List do
# Get lists to which the account belongs.
def get_lists_account_belongs(%User{} = owner, account_id) do
user = Repo.get(User, account_id)
user = User.get_cached_by_id(account_id)
query =
from(

View file

@ -7,12 +7,15 @@ defmodule Pleroma.Notification do
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
import Ecto.Query
import Ecto.Changeset
schema "notifications" do
field(:seen, :boolean, default: false)
@ -22,36 +25,30 @@ defmodule Pleroma.Notification do
timestamps()
end
# TODO: Make generic and unify (see activity_pub.ex)
defp restrict_max(query, %{"max_id" => max_id}) do
from(activity in query, where: activity.id < ^max_id)
def changeset(%Notification{} = notification, attrs) do
notification
|> cast(attrs, [:seen])
end
defp restrict_max(query, _), do: query
defp restrict_since(query, %{"since_id" => since_id}) do
from(activity in query, where: activity.id > ^since_id)
def for_user_query(user) do
Notification
|> where(user_id: ^user.id)
|> join(:inner, [n], activity in assoc(n, :activity))
|> join(:left, [n, a], object in Object,
on:
fragment(
"(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
object.data,
a.data
)
)
|> preload([n, a, o], activity: {a, object: o})
end
defp restrict_since(query, _), do: query
def for_user(user, opts \\ %{}) do
query =
from(
n in Notification,
where: n.user_id == ^user.id,
order_by: [desc: n.id],
join: activity in assoc(n, :activity),
preload: [activity: activity],
limit: 20
)
query =
query
|> restrict_since(opts)
|> restrict_max(opts)
Repo.all(query)
user
|> for_user_query()
|> Pagination.fetch_paginated(opts)
end
def set_read_up_to(%{id: user_id} = _user, id) do
@ -68,6 +65,14 @@ defmodule Pleroma.Notification do
Repo.update_all(query, [])
end
def read_one(%User{} = user, notification_id) do
with {:ok, %Notification{} = notification} <- get(user, notification_id) do
notification
|> changeset(%{seen: true})
|> Repo.update()
end
end
def get(%{id: user_id} = _user, id) do
query =
from(
@ -93,6 +98,14 @@ defmodule Pleroma.Notification do
|> Repo.delete_all()
end
def destroy_multiple(%{id: user_id} = _user, ids) do
from(n in Notification,
where: n.id in ^ids,
where: n.user_id == ^user_id
)
|> Repo.delete_all()
end
def dismiss(%{id: user_id} = _user, id) do
notification = Repo.get(Notification, id)
@ -117,13 +130,7 @@ defmodule Pleroma.Notification do
# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user) do
unless User.blocks?(user, %{ap_id: activity.data["actor"]}) or
CommonAPI.thread_muted?(user, activity) or user.ap_id == activity.data["actor"] or
(activity.data["type"] == "Follow" and
Enum.any?(Notification.for_user(user), fn notif ->
notif.activity.data["type"] == "Follow" and
notif.activity.data["actor"] == activity.data["actor"]
end)) do
unless skip?(activity, user) do
notification = %Notification{user_id: user.id, activity: activity}
{:ok, notification} = Repo.insert(notification)
Pleroma.Web.Streamer.stream("user", notification)
@ -143,10 +150,65 @@ defmodule Pleroma.Notification do
[]
|> Utils.maybe_notify_to_recipients(activity)
|> Utils.maybe_notify_mentioned_recipients(activity)
|> Utils.maybe_notify_subscribers(activity)
|> Enum.uniq()
User.get_users_from_set(recipients, local_only)
end
def get_notified_from_activity(_, _local_only), do: []
def skip?(activity, user) do
[:self, :blocked, :local, :muted, :followers, :follows, :recently_followed]
|> Enum.any?(&skip?(&1, activity, user))
end
def skip?(:self, activity, user) do
activity.data["actor"] == user.ap_id
end
def skip?(:blocked, activity, user) do
actor = activity.data["actor"]
User.blocks?(user, %{ap_id: actor})
end
def skip?(:local, %{local: true}, %{info: %{notification_settings: %{"local" => false}}}),
do: true
def skip?(:local, %{local: false}, %{info: %{notification_settings: %{"remote" => false}}}),
do: true
def skip?(:muted, activity, user) do
actor = activity.data["actor"]
User.mutes?(user, %{ap_id: actor}) or CommonAPI.thread_muted?(user, activity)
end
def skip?(
:followers,
activity,
%{info: %{notification_settings: %{"followers" => false}}} = user
) do
actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor)
User.following?(follower, user)
end
def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do
actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor)
User.following?(user, followed)
end
def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do
actor = activity.data["actor"]
Notification.for_user(user)
|> Enum.any?(fn
%{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
_ -> false
end)
end
def skip?(_, _, _), do: false
end

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Object do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.ObjectTombstone
alias Pleroma.Repo
alias Pleroma.User
@ -14,6 +15,8 @@ defmodule Pleroma.Object do
import Ecto.Query
import Ecto.Changeset
require Logger
schema "objects" do
field(:data, :map)
@ -38,9 +41,44 @@ defmodule Pleroma.Object do
Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
end
def normalize(%{"id" => ap_id}), do: normalize(ap_id)
def normalize(ap_id) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
def normalize(_), do: nil
def normalize(_, fetch_remote \\ true)
# If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
# Use this whenever possible, especially when walking graphs in an O(N) loop!
def normalize(%Object{} = object, _), do: object
def normalize(%Activity{object: %Object{} = object}, _), do: object
# A hack for fake activities
def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _) do
%Object{id: "pleroma:fake_object_id", data: data}
end
# Catch and log Object.normalize() calls where the Activity's child object is not
# preloaded.
def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote) do
Logger.debug(
"Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object!"
)
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
normalize(ap_id, fetch_remote)
end
def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote) do
Logger.debug(
"Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object!"
)
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
normalize(ap_id, fetch_remote)
end
# Old way, try fetching the object through cache.
def normalize(%{"id" => ap_id}, fetch_remote), do: normalize(ap_id, fetch_remote)
def normalize(ap_id, false) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
def normalize(ap_id, true) when is_binary(ap_id), do: Fetcher.fetch_object_from_id!(ap_id)
def normalize(_, _), do: nil
# Owned objects can only be mutated by their owner
def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
@ -104,4 +142,50 @@ defmodule Pleroma.Object do
e -> e
end
end
def increase_replies_count(ap_id) do
Object
|> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
|> update([o],
set: [
data:
fragment(
"""
jsonb_set(?, '{repliesCount}',
(coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
""",
o.data,
o.data
)
]
)
|> Repo.update_all([])
|> case do
{1, [object]} -> set_cache(object)
_ -> {:error, "Not found"}
end
end
def decrease_replies_count(ap_id) do
Object
|> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
|> update([o],
set: [
data:
fragment(
"""
jsonb_set(?, '{repliesCount}',
(greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
""",
o.data,
o.data
)
]
)
|> Repo.update_all([])
|> case do
{1, [object]} -> set_cache(object)
_ -> {:error, "Not found"}
end
end
end

View file

@ -0,0 +1,61 @@
defmodule Pleroma.Object.Containment do
@moduledoc """
# Object Containment
This module contains some useful functions for containing objects to specific
origins and determining those origins. They previously lived in the
ActivityPub `Transmogrifier` module.
Object containment is an important step in validating remote objects to prevent
spoofing, therefore removal of object containment functions is NOT recommended.
"""
def get_actor(%{"actor" => actor}) when is_binary(actor) do
actor
end
def get_actor(%{"actor" => actor}) when is_list(actor) do
if is_binary(Enum.at(actor, 0)) do
Enum.at(actor, 0)
else
Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end)
|> Map.get("id")
end
end
def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do
id
end
def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
get_actor(%{"actor" => actor})
end
@doc """
Checks that an imported AP object's actor matches the domain it came from.
"""
def contain_origin(_id, %{"actor" => nil}), do: :error
def contain_origin(id, %{"actor" => _actor} = params) do
id_uri = URI.parse(id)
actor_uri = URI.parse(get_actor(params))
if id_uri.host == actor_uri.host do
:ok
else
:error
end
end
def contain_origin_from_id(_id, %{"id" => nil}), do: :error
def contain_origin_from_id(id, %{"id" => other_id} = _params) do
id_uri = URI.parse(id)
other_uri = URI.parse(other_id)
if id_uri.host == other_uri.host do
:ok
else
:error
end
end
end

View file

@ -0,0 +1,75 @@
defmodule Pleroma.Object.Fetcher do
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.OStatus
require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
# TODO:
# This will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id) do
if object = Object.get_cached_by_ap_id(id) do
{:ok, object}
else
Logger.info("Fetching #{id} via AP")
with {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
nil <- Object.normalize(data, false),
params <- %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => data["actor"] || data["attributedTo"],
"object" => data
},
:ok <- Containment.contain_origin(id, params),
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, Object.normalize(activity, false)}
else
{:error, {:reject, nil}} ->
{:reject, nil}
object = %Object{} ->
{:ok, object}
_e ->
Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
e -> e
end
end
end
end
def fetch_object_from_id!(id) do
with {:ok, object} <- fetch_object_from_id(id) do
object
else
_e ->
nil
end
end
def fetch_and_contain_remote_object_from_id(id) do
Logger.info("Fetching object #{id} via AP")
with true <- String.starts_with?(id, "http"),
{:ok, %{body: body, status: code}} when code in 200..299 <-
@httpoison.get(
id,
[{:Accept, "application/activity+json"}]
),
{:ok, data} <- Jason.decode(body),
:ok <- Containment.contain_origin_from_id(id, data) do
{:ok, data}
else
e ->
{:error, e}
end
end
end

84
lib/pleroma/pagination.ex Normal file
View file

@ -0,0 +1,84 @@
defmodule Pleroma.Pagination do
@moduledoc """
Implements Mastodon-compatible pagination.
"""
import Ecto.Query
import Ecto.Changeset
alias Pleroma.Repo
@default_limit 20
def fetch_paginated(query, params) do
options = cast_params(params)
query
|> paginate(options)
|> Repo.all()
|> enforce_order(options)
end
def paginate(query, options) do
query
|> restrict(:min_id, options)
|> restrict(:since_id, options)
|> restrict(:max_id, options)
|> restrict(:order, options)
|> restrict(:limit, options)
end
defp cast_params(params) do
param_types = %{
min_id: :string,
since_id: :string,
max_id: :string,
limit: :integer
}
params =
Enum.reduce(params, %{}, fn
{key, _value}, acc when is_atom(key) -> Map.drop(acc, [key])
{key, value}, acc -> Map.put(acc, key, value)
end)
changeset = cast({%{}, param_types}, params, Map.keys(param_types))
changeset.changes
end
defp restrict(query, :min_id, %{min_id: min_id}) do
where(query, [q], q.id > ^min_id)
end
defp restrict(query, :since_id, %{since_id: since_id}) do
where(query, [q], q.id > ^since_id)
end
defp restrict(query, :max_id, %{max_id: max_id}) do
where(query, [q], q.id < ^max_id)
end
defp restrict(query, :order, %{min_id: _}) do
order_by(query, [u], fragment("? asc nulls last", u.id))
end
defp restrict(query, :order, _options) do
order_by(query, [u], fragment("? desc nulls last", u.id))
end
defp restrict(query, :limit, options) do
limit = Map.get(options, :limit, @default_limit)
query
|> limit(^limit)
end
defp restrict(query, _, _), do: query
defp enforce_order(result, %{min_id: _}) do
result
|> Enum.reverse()
end
defp enforce_order(result, _), do: result
end

View file

@ -21,7 +21,8 @@ defmodule Pleroma.Plugs.InstanceStatic do
end
end
@only ~w(index.html static emoji packs sounds images instance favicon.png sw.js sw-pleroma.js)
@only ~w(index.html robots.txt static emoji packs sounds images instance favicon.png sw.js
sw-pleroma.js)
def init(opts) do
opts

View file

@ -24,6 +24,18 @@ defmodule Pleroma.Plugs.UploadedMedia do
end
def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
conn =
case fetch_query_params(conn) do
%{query_params: %{"name" => name}} = conn ->
name = String.replace(name, "\"", "\\\"")
conn
|> put_resp_header("content-disposition", "filename=\"#{name}\"")
conn ->
conn
end
config = Pleroma.Config.get([Pleroma.Upload])
with uploader <- Keyword.fetch!(config, :uploader),

View file

@ -3,9 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.UserFetcherPlug do
alias Pleroma.Repo
alias Pleroma.User
import Plug.Conn
def init(options) do
@ -14,26 +12,10 @@ defmodule Pleroma.Plugs.UserFetcherPlug do
def call(conn, _options) do
with %{auth_credentials: %{username: username}} <- conn.assigns,
{:ok, %User{} = user} <- user_fetcher(username) do
conn
|> assign(:auth_user, user)
%User{} = user <- User.get_by_nickname_or_email(username) do
assign(conn, :auth_user, user)
else
_ -> conn
end
end
defp user_fetcher(username_or_email) do
{
:ok,
cond do
# First, try logging in as if it was a name
user = Repo.get_by(User, %{nickname: username_or_email}) ->
user
# If we get nil, we try using it as an email
user = Repo.get_by(User, %{email: username_or_email}) ->
user
end
}
end
end

View file

@ -0,0 +1,57 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Registration do
use Ecto.Schema
import Ecto.Changeset
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
@primary_key {:id, Pleroma.FlakeId, autogenerate: true}
schema "registrations" do
belongs_to(:user, User, type: Pleroma.FlakeId)
field(:provider, :string)
field(:uid, :string)
field(:info, :map, default: %{})
timestamps()
end
def nickname(registration, default \\ nil),
do: Map.get(registration.info, "nickname", default)
def email(registration, default \\ nil),
do: Map.get(registration.info, "email", default)
def name(registration, default \\ nil),
do: Map.get(registration.info, "name", default)
def description(registration, default \\ nil),
do: Map.get(registration.info, "description", default)
def changeset(registration, params \\ %{}) do
registration
|> cast(params, [:user_id, :provider, :uid, :info])
|> validate_required([:provider, :uid])
|> foreign_key_constraint(:user_id)
|> unique_constraint(:uid, name: :registrations_provider_uid_index)
end
def bind_to_user(registration, user) do
registration
|> changeset(%{user_id: (user && user.id) || nil})
|> Repo.update()
end
def get_by_provider_uid(provider, uid) do
Repo.get_by(Registration,
provider: to_string(provider),
uid: to_string(uid)
)
end
end

View file

@ -3,7 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Repo do
use Ecto.Repo, otp_app: :pleroma
use Ecto.Repo,
otp_app: :pleroma,
adapter: Ecto.Adapters.Postgres,
migration_timestamps: [type: :naive_datetime_usec]
defmodule Instrumenter do
use Prometheus.EctoInstrumenter
end
@doc """
Dynamically loads the repository url from the

View file

@ -311,7 +311,25 @@ defmodule Pleroma.ReverseProxy do
end
if attachment? do
disposition = "attachment; filename=" <> Keyword.get(opts, :attachment_name, "attachment")
name =
try do
{{"content-disposition", content_disposition_string}, _} =
List.keytake(headers, "content-disposition", 0)
[name | _] =
Regex.run(
~r/filename="((?:[^"\\]|\\.)*)"/u,
content_disposition_string || "",
capture: :all_but_first
)
name
rescue
MatchError -> Keyword.get(opts, :attachment_name, "attachment")
end
disposition = "attachment; filename=\"#{name}\""
List.keystore(headers, "content-disposition", 0, {"content-disposition", disposition})
else
headers

View file

@ -0,0 +1,161 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ScheduledActivity do
use Ecto.Schema
alias Pleroma.Config
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
import Ecto.Query
import Ecto.Changeset
@min_offset :timer.minutes(5)
schema "scheduled_activities" do
belongs_to(:user, User, type: Pleroma.FlakeId)
field(:scheduled_at, :naive_datetime)
field(:params, :map)
timestamps()
end
def changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
scheduled_activity
|> cast(attrs, [:scheduled_at, :params])
|> validate_required([:scheduled_at, :params])
|> validate_scheduled_at()
|> with_media_attachments()
end
defp with_media_attachments(
%{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
)
when is_list(media_ids) do
media_attachments = Utils.attachments_from_ids(%{"media_ids" => media_ids})
params =
params
|> Map.put("media_attachments", media_attachments)
|> Map.put("media_ids", media_ids)
put_change(changeset, :params, params)
end
defp with_media_attachments(changeset), do: changeset
def update_changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
scheduled_activity
|> cast(attrs, [:scheduled_at])
|> validate_required([:scheduled_at])
|> validate_scheduled_at()
end
def validate_scheduled_at(changeset) do
validate_change(changeset, :scheduled_at, fn _, scheduled_at ->
cond do
not far_enough?(scheduled_at) ->
[scheduled_at: "must be at least 5 minutes from now"]
exceeds_daily_user_limit?(changeset.data.user_id, scheduled_at) ->
[scheduled_at: "daily limit exceeded"]
exceeds_total_user_limit?(changeset.data.user_id) ->
[scheduled_at: "total limit exceeded"]
true ->
[]
end
end)
end
def exceeds_daily_user_limit?(user_id, scheduled_at) do
ScheduledActivity
|> where(user_id: ^user_id)
|> where([sa], type(sa.scheduled_at, :date) == type(^scheduled_at, :date))
|> select([sa], count(sa.id))
|> Repo.one()
|> Kernel.>=(Config.get([ScheduledActivity, :daily_user_limit]))
end
def exceeds_total_user_limit?(user_id) do
ScheduledActivity
|> where(user_id: ^user_id)
|> select([sa], count(sa.id))
|> Repo.one()
|> Kernel.>=(Config.get([ScheduledActivity, :total_user_limit]))
end
def far_enough?(scheduled_at) when is_binary(scheduled_at) do
with {:ok, scheduled_at} <- Ecto.Type.cast(:naive_datetime, scheduled_at) do
far_enough?(scheduled_at)
else
_ -> false
end
end
def far_enough?(scheduled_at) do
now = NaiveDateTime.utc_now()
diff = NaiveDateTime.diff(scheduled_at, now, :millisecond)
diff > @min_offset
end
def new(%User{} = user, attrs) do
%ScheduledActivity{user_id: user.id}
|> changeset(attrs)
end
def create(%User{} = user, attrs) do
user
|> new(attrs)
|> Repo.insert()
end
def get(%User{} = user, scheduled_activity_id) do
ScheduledActivity
|> where(user_id: ^user.id)
|> where(id: ^scheduled_activity_id)
|> Repo.one()
end
def update(%ScheduledActivity{} = scheduled_activity, attrs) do
scheduled_activity
|> update_changeset(attrs)
|> Repo.update()
end
def delete(%ScheduledActivity{} = scheduled_activity) do
scheduled_activity
|> Repo.delete()
end
def delete(id) when is_binary(id) or is_integer(id) do
ScheduledActivity
|> where(id: ^id)
|> select([sa], sa)
|> Repo.delete_all()
|> case do
{1, [scheduled_activity]} -> {:ok, scheduled_activity}
_ -> :error
end
end
def for_user_query(%User{} = user) do
ScheduledActivity
|> where(user_id: ^user.id)
end
def due_activities(offset \\ 0) do
naive_datetime =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(offset, :millisecond)
ScheduledActivity
|> where([sa], sa.scheduled_at < ^naive_datetime)
|> Repo.all()
end
end

View file

@ -0,0 +1,58 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ScheduledActivityWorker do
@moduledoc """
Sends scheduled activities to the job queue.
"""
alias Pleroma.Config
alias Pleroma.ScheduledActivity
alias Pleroma.User
alias Pleroma.Web.CommonAPI
use GenServer
require Logger
@schedule_interval :timer.minutes(1)
def start_link do
GenServer.start_link(__MODULE__, nil)
end
def init(_) do
if Config.get([ScheduledActivity, :enabled]) do
schedule_next()
{:ok, nil}
else
:ignore
end
end
def perform(:execute, scheduled_activity_id) do
try do
{:ok, scheduled_activity} = ScheduledActivity.delete(scheduled_activity_id)
%User{} = user = User.get_cached_by_id(scheduled_activity.user_id)
{:ok, _result} = CommonAPI.post(user, scheduled_activity.params)
rescue
error ->
Logger.error(
"#{__MODULE__} Couldn't create a status from the scheduled activity: #{inspect(error)}"
)
end
end
def handle_info(:perform, state) do
ScheduledActivity.due_activities(@schedule_interval)
|> Enum.each(fn scheduled_activity ->
PleromaJobQueue.enqueue(:scheduled_activities, __MODULE__, [:execute, scheduled_activity.id])
end)
schedule_next()
{:noreply, state}
end
defp schedule_next do
Process.send_after(self(), :perform, @schedule_interval)
end
end

View file

@ -70,7 +70,7 @@ defmodule Pleroma.Upload do
%{
"type" => "Link",
"mediaType" => upload.content_type,
"href" => url_from_spec(opts.base_url, url_spec)
"href" => url_from_spec(upload, opts.base_url, url_spec)
}
],
"name" => Map.get(opts, :description) || upload.name
@ -219,14 +219,18 @@ defmodule Pleroma.Upload do
tmp_path
end
defp url_from_spec(base_url, {:file, path}) do
defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
path =
path
|> URI.encode(&char_unescaped?/1)
URI.encode(path, &char_unescaped?/1) <>
if Pleroma.Config.get([__MODULE__, :link_name], false) do
"?name=#{URI.encode(name, &char_unescaped?/1)}"
else
""
end
[base_url, "media", path]
|> Path.join()
end
defp url_from_spec(_base_url, {:url, url}), do: url
defp url_from_spec(_upload, _base_url, {:url, url}), do: url
end

View file

@ -13,10 +13,15 @@ defmodule Pleroma.Uploaders.S3 do
bucket = Keyword.fetch!(config, :bucket)
bucket_with_namespace =
if namespace = Keyword.get(config, :bucket_namespace) do
namespace <> ":" <> bucket
else
bucket
cond do
truncated_namespace = Keyword.get(config, :truncated_namespace) ->
truncated_namespace
namespace = Keyword.get(config, :bucket_namespace) ->
namespace <> ":" <> bucket
true ->
bucket
end
{:ok,

View file

@ -10,9 +10,11 @@ defmodule Pleroma.User do
alias Comeonin.Pbkdf2
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Formatter
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web
@ -50,23 +52,21 @@ defmodule Pleroma.User do
field(:local, :boolean, default: true)
field(:follower_address, :string)
field(:search_rank, :float, virtual: true)
field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: [])
field(:bookmarks, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime)
field(:last_refreshed_at, :naive_datetime_usec)
has_many(:bookmarks, Bookmark)
has_many(:notifications, Notification)
has_many(:registrations, Registration)
embeds_one(:info, Pleroma.User.Info)
timestamps()
end
def auth_active?(%User{local: false}), do: true
def auth_active?(%User{info: %User.Info{confirmation_pending: false}}), do: true
def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
do: !Pleroma.Config.get([:instance, :account_activation_required])
def auth_active?(_), do: false
def auth_active?(%User{}), do: true
def visible_for?(user, for_user \\ nil)
@ -82,17 +82,17 @@ defmodule Pleroma.User do
def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
def superuser?(_), do: false
def avatar_url(user) do
def avatar_url(user, options \\ []) do
case user.avatar do
%{"url" => [%{"href" => href} | _]} -> href
_ -> "#{Web.base_url()}/images/avi.png"
_ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
end
end
def banner_url(user) do
def banner_url(user, options \\ []) do
case user.info.banner do
%{"url" => [%{"href" => href} | _]} -> href
_ -> "#{Web.base_url()}/images/banner.png"
_ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
end
end
@ -104,9 +104,8 @@ defmodule Pleroma.User do
"#{Web.base_url()}/users/#{nickname}"
end
def ap_followers(%User{} = user) do
"#{ap_id(user)}/followers"
end
def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
def user_info(%User{} = user) do
oneself = if user.local, do: 1, else: 0
@ -220,7 +219,7 @@ defmodule Pleroma.User do
changeset =
struct
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
|> validate_required([:email, :name, :nickname, :password, :password_confirmation])
|> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> unique_constraint(:nickname)
@ -231,6 +230,13 @@ defmodule Pleroma.User do
|> validate_length(:name, min: 1, max: 100)
|> put_change(:info, info_change)
changeset =
if opts[:external] do
changeset
else
validate_required(changeset, [:email])
end
if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
@ -264,6 +270,7 @@ defmodule Pleroma.User do
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset),
{:ok, user} <- autofollow_users(user),
{:ok, user} <- set_cache(user),
{:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
{:ok, _} <- try_send_confirmation_email(user) do
{:ok, user}
@ -274,8 +281,10 @@ defmodule Pleroma.User do
if user.info.confirmation_pending &&
Pleroma.Config.get([:instance, :account_activation_required]) do
user
|> Pleroma.UserEmail.account_confirmation_email()
|> Pleroma.Mailer.deliver_async()
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|> Pleroma.Emails.Mailer.deliver_async()
{:ok, :enqueued}
else
{:ok, :noop}
end
@ -335,10 +344,11 @@ defmodule Pleroma.User do
^followed_addresses
)
]
]
],
select: u
)
{1, [follower]} = Repo.update_all(q, [], returning: true)
{1, [follower]} = Repo.update_all(q, [])
Enum.each(followeds, fn followed ->
update_follower_count(followed)
@ -368,10 +378,11 @@ defmodule Pleroma.User do
q =
from(u in User,
where: u.id == ^follower.id,
update: [push: [following: ^ap_followers]]
update: [push: [following: ^ap_followers]],
select: u
)
{1, [follower]} = Repo.update_all(q, [], returning: true)
{1, [follower]} = Repo.update_all(q, [])
{:ok, _} = update_follower_count(followed)
@ -386,10 +397,11 @@ defmodule Pleroma.User do
q =
from(u in User,
where: u.id == ^follower.id,
update: [pull: [following: ^ap_followers]]
update: [pull: [following: ^ap_followers]],
select: u
)
{1, [follower]} = Repo.update_all(q, [], returning: true)
{1, [follower]} = Repo.update_all(q, [])
{:ok, followed} = update_follower_count(followed)
@ -443,10 +455,13 @@ defmodule Pleroma.User do
name = List.last(String.split(ap_id, "/"))
nickname = "#{name}@#{domain}"
get_by_nickname(nickname)
get_cached_by_nickname(nickname)
end
def set_cache(user) do
def set_cache({:ok, user}), do: set_cache(user)
def set_cache({:error, err}), do: {:error, err}
def set_cache(%User{} = user) do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
@ -514,11 +529,10 @@ defmodule Pleroma.User do
end
end
def get_by_email(email), do: Repo.get_by(User, email: email)
def get_by_nickname_or_email(nickname_or_email) do
case user = Repo.get_by(User, nickname: nickname_or_email) do
%User{} -> user
nil -> Repo.get_by(User, email: nickname_or_email)
end
get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
end
def get_cached_user_info(user) do
@ -543,6 +557,7 @@ defmodule Pleroma.User do
with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- fetch_by_nickname(nickname) do
if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
# TODO turn into job
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
end
@ -645,7 +660,7 @@ defmodule Pleroma.User do
users =
user
|> User.get_follow_requests_query()
|> join(:inner, [a], u in User, a.actor == u.ap_id)
|> join(:inner, [a], u in User, on: a.actor == u.ap_id)
|> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
|> group_by([a, u], u.id)
|> select([a, u], u)
@ -667,7 +682,8 @@ defmodule Pleroma.User do
)
]
)
|> Repo.update_all([], returning: true)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
@ -687,7 +703,8 @@ defmodule Pleroma.User do
)
]
)
|> Repo.update_all([], returning: true)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
@ -733,7 +750,8 @@ defmodule Pleroma.User do
)
]
)
|> Repo.update_all([], returning: true)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
@ -774,90 +792,59 @@ defmodule Pleroma.User do
Repo.all(query)
end
@spec search_for_admin(%{
local: boolean(),
page: number(),
page_size: number()
}) :: {:ok, [Pleroma.User.t()], number()}
def search_for_admin(%{query: nil, local: local, page: page, page_size: page_size}) do
query =
from(u in User, order_by: u.id)
|> maybe_local_user_query(local)
paginated_query =
query
|> paginate(page, page_size)
count =
query
|> Repo.aggregate(:count, :id)
{:ok, Repo.all(paginated_query), count}
end
@spec search_for_admin(%{
query: binary(),
admin: Pleroma.User.t(),
local: boolean(),
page: number(),
page_size: number()
}) :: {:ok, [Pleroma.User.t()], number()}
def search_for_admin(%{
query: term,
admin: admin,
local: local,
page: page,
page_size: page_size
}) do
term = String.trim_leading(term, "@")
local_paginated_query =
User
|> maybe_local_user_query(local)
|> paginate(page, page_size)
search_query = fts_search_subquery(term, local_paginated_query)
count =
term
|> fts_search_subquery()
|> maybe_local_user_query(local)
|> Repo.aggregate(:count, :id)
{:ok, do_search(search_query, admin), count}
end
def search(query, resolve \\ false, for_user \\ nil) do
# Strip the beginning @ off if there is a query
query = String.trim_leading(query, "@")
if resolve, do: get_or_fetch(query)
fts_results = do_search(fts_search_subquery(query), for_user)
{:ok, trigram_results} =
{:ok, results} =
Repo.transaction(fn ->
Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
do_search(trigram_search_subquery(query), for_user)
Repo.all(search_query(query, for_user))
end)
Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
results
end
defp do_search(subquery, for_user, options \\ []) do
q =
from(
s in subquery(subquery),
order_by: [desc: s.search_rank],
limit: ^(options[:limit] || 20)
)
def search_query(query, for_user) do
fts_subquery = fts_search_subquery(query)
trigram_subquery = trigram_search_subquery(query)
union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
results =
q
|> Repo.all()
|> Enum.filter(&(&1.search_rank > 0))
from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
order_by: [desc: s.search_rank],
limit: 20
)
end
boost_search_results(results, for_user)
defp boost_search_rank_query(query, nil), do: query
defp boost_search_rank_query(query, for_user) do
friends_ids = get_friends_ids(for_user)
followers_ids = get_followers_ids(for_user)
from(u in subquery(query),
select_merge: %{
search_rank:
fragment(
"""
CASE WHEN (?) THEN (?) * 1.3
WHEN (?) THEN (?) * 1.2
WHEN (?) THEN (?) * 1.1
ELSE (?) END
""",
u.id in ^friends_ids and u.id in ^followers_ids,
u.search_rank,
u.id in ^friends_ids,
u.search_rank,
u.id in ^followers_ids,
u.search_rank,
u.search_rank
)
}
)
end
defp fts_search_subquery(term, query \\ User) do
@ -872,6 +859,7 @@ defmodule Pleroma.User do
from(
u in query,
select_merge: %{
search_type: ^0,
search_rank:
fragment(
"""
@ -904,6 +892,8 @@ defmodule Pleroma.User do
from(
u in User,
select_merge: %{
# ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
search_type: fragment("?", 1),
search_rank:
fragment(
"similarity(?, trim(? || ' ' || coalesce(?, '')))",
@ -916,33 +906,6 @@ defmodule Pleroma.User do
)
end
defp boost_search_results(results, nil), do: results
defp boost_search_results(results, for_user) do
friends_ids = get_friends_ids(for_user)
followers_ids = get_followers_ids(for_user)
Enum.map(
results,
fn u ->
search_rank_coef =
cond do
u.id in friends_ids ->
1.2
u.id in followers_ids ->
1.1
true ->
1
end
Map.put(u, :search_rank, u.search_rank * search_rank_coef)
end
)
|> Enum.sort_by(&(-&1.search_rank))
end
def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
Enum.map(
blocked_identifiers,
@ -984,6 +947,38 @@ defmodule Pleroma.User do
update_and_set_cache(cng)
end
def subscribe(subscriber, %{ap_id: ap_id}) do
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
if blocked do
{:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
else
info_cng =
subscribed.info
|> User.Info.add_to_subscribers(subscriber.ap_id)
change(subscribed)
|> put_embed(:info, info_cng)
|> update_and_set_cache()
end
end
end
def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
with %User{} = user <- get_cached_by_ap_id(ap_id) do
info_cng =
user.info
|> User.Info.remove_from_subscribers(unsubscriber.ap_id)
change(user)
|> put_embed(:info, info_cng)
|> update_and_set_cache()
end
end
def block(blocker, %User{ap_id: ap_id} = blocked) do
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
blocker =
@ -994,10 +989,20 @@ defmodule Pleroma.User do
blocker
end
blocker =
if subscribed_to?(blocked, blocker) do
{:ok, blocker} = unsubscribe(blocked, blocker)
blocker
else
blocker
end
if following?(blocked, blocker) do
unfollow(blocked, blocker)
end
{:ok, blocker} = update_follower_count(blocker)
info_cng =
blocker.info
|> User.Info.add_to_block(ap_id)
@ -1011,7 +1016,7 @@ defmodule Pleroma.User do
# helper to handle the block given only an actor's AP id
def block(blocker, %{ap_id: ap_id}) do
block(blocker, User.get_by_ap_id(ap_id))
block(blocker, get_cached_by_ap_id(ap_id))
end
def unblock(blocker, %{ap_id: ap_id}) do
@ -1040,12 +1045,21 @@ defmodule Pleroma.User do
end)
end
def subscribed_to?(user, %{ap_id: ap_id}) do
with %User{} = target <- get_cached_by_ap_id(ap_id) do
Enum.member?(target.info.subscribers, user.ap_id)
end
end
def muted_users(user),
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
def blocked_users(user),
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
def subscribers(user),
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.subscribers))
def block_domain(user, domain) do
info_cng =
user.info
@ -1082,6 +1096,42 @@ defmodule Pleroma.User do
)
end
def maybe_external_user_query(query, external) do
if external, do: external_user_query(query), else: query
end
def external_user_query(query \\ User) do
from(
u in query,
where: u.local == false,
where: not is_nil(u.nickname)
)
end
def maybe_active_user_query(query, active) do
if active, do: active_user_query(query), else: query
end
def active_user_query(query \\ User) do
from(
u in query,
where: fragment("not (?->'deactivated' @> 'true')", u.info),
where: not is_nil(u.nickname)
)
end
def maybe_deactivated_user_query(query, deactivated) do
if deactivated, do: deactivated_user_query(query), else: query
end
def deactivated_user_query(query \\ User) do
from(
u in query,
where: fragment("(?->'deactivated' @> 'true')", u.info),
where: not is_nil(u.nickname)
)
end
def active_local_user_query do
from(
u in local_user_query(),
@ -1107,32 +1157,41 @@ defmodule Pleroma.User do
update_and_set_cache(cng)
end
def update_notification_settings(%User{} = user, settings \\ %{}) do
info_changeset = User.Info.update_notification_settings(user.info, settings)
change(user)
|> put_embed(:info, info_changeset)
|> update_and_set_cache()
end
def delete(%User{} = user) do
{:ok, user} = User.deactivate(user)
# Remove all relationships
{:ok, followers} = User.get_followers(user)
followers
|> Enum.each(fn follower -> User.unfollow(follower, user) end)
Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
{:ok, friends} = User.get_friends(user)
friends
|> Enum.each(fn followed -> User.unfollow(user, followed) end)
Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
query = from(a in Activity, where: a.actor == ^user.ap_id)
delete_user_activities(user)
end
Repo.all(query)
|> Enum.each(fn activity ->
case activity.data["type"] do
"Create" ->
ActivityPub.delete(Object.normalize(activity.data["object"]))
def delete_user_activities(%User{ap_id: ap_id} = user) do
Activity
|> where(actor: ^ap_id)
|> Activity.with_preloaded_object()
|> Repo.all()
|> Enum.each(fn
%{data: %{"type" => "Create"}} = activity ->
activity |> Object.normalize() |> ActivityPub.delete()
# TODO: Do something with likes, follows, repeats.
_ ->
"Doing nothing"
end
# TODO: Do something with likes, follows, repeats.
_ ->
"Doing nothing"
end)
{:ok, user}
@ -1162,13 +1221,16 @@ defmodule Pleroma.User do
end
def get_or_fetch_by_ap_id(ap_id) do
user = get_by_ap_id(ap_id)
user = get_cached_by_ap_id(ap_id)
if !is_nil(user) and !User.needs_update?(user) do
{:ok, user}
else
with %User{} = user <- fetch_by_ap_id(ap_id) do
if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
# Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
if should_fetch_initial do
with {:ok, %User{} = user} = fetch_by_ap_id(ap_id) do
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
end
@ -1182,7 +1244,7 @@ defmodule Pleroma.User do
def get_or_create_instance_user do
relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
if user = get_by_ap_id(relay_uri) do
if user = get_cached_by_ap_id(relay_uri) do
user
else
changes =
@ -1229,13 +1291,11 @@ defmodule Pleroma.User do
defp blank?(n), do: n
def insert_or_update_user(data) do
data =
data
|> Map.put(:name, blank?(data[:name]) || data[:nickname])
cs = User.remote_user_creation(data)
Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
data
|> Map.put(:name, blank?(data[:name]) || data[:nickname])
|> remote_user_creation()
|> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
|> set_cache()
end
def ap_enabled?(%User{local: true}), do: true
@ -1251,8 +1311,8 @@ defmodule Pleroma.User do
# this is because we have synchronous follow APIs and need to simulate them
# with an async handshake
def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
with %User{} = a <- Repo.get(User, a.id),
%User{} = b <- Repo.get(User, b.id) do
with %User{} = a <- User.get_cached_by_id(a.id),
%User{} = b <- User.get_cached_by_id(b.id) do
{:ok, a, b}
else
_e ->
@ -1262,8 +1322,8 @@ defmodule Pleroma.User do
def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
with :ok <- :timer.sleep(timeout),
%User{} = a <- Repo.get(User, a.id),
%User{} = b <- Repo.get(User, b.id) do
%User{} = a <- User.get_cached_by_id(a.id),
%User{} = b <- User.get_cached_by_id(b.id) do
{:ok, a, b}
else
_e ->
@ -1302,7 +1362,7 @@ defmodule Pleroma.User do
end
def tag(nickname, tags) when is_binary(nickname),
do: tag(User.get_by_nickname(nickname), tags)
do: tag(get_by_nickname(nickname), tags)
def tag(%User{} = user, tags),
do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
@ -1314,7 +1374,7 @@ defmodule Pleroma.User do
end
def untag(nickname, tags) when is_binary(nickname),
do: untag(User.get_by_nickname(nickname), tags)
do: untag(get_by_nickname(nickname), tags)
def untag(%User{} = user, tags),
do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
@ -1328,22 +1388,6 @@ defmodule Pleroma.User do
updated_user
end
def bookmark(%User{} = user, status_id) do
bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
update_bookmarks(user, bookmarks)
end
def unbookmark(%User{} = user, status_id) do
bookmarks = Enum.uniq(user.bookmarks -- [status_id])
update_bookmarks(user, bookmarks)
end
def update_bookmarks(%User{} = user, bookmarks) do
user
|> change(%{bookmarks: bookmarks})
|> update_and_set_cache
end
defp normalize_tags(tags) do
[tags]
|> List.flatten()
@ -1393,4 +1437,8 @@ defmodule Pleroma.User do
offset: ^((page - 1) * page_size)
)
end
def showing_reblogs?(%User{} = user, %User{} = target) do
target.ap_id not in user.info.muted_reblogs
end
end

View file

@ -21,6 +21,8 @@ defmodule Pleroma.User.Info do
field(:blocks, {:array, :string}, default: [])
field(:domain_blocks, {:array, :string}, default: [])
field(:mutes, {:array, :string}, default: [])
field(:muted_reblogs, {:array, :string}, default: [])
field(:subscribers, {:array, :string}, default: [])
field(:deactivated, :boolean, default: false)
field(:no_rich_text, :boolean, default: false)
field(:ap_enabled, :boolean, default: false)
@ -36,9 +38,14 @@ defmodule Pleroma.User.Info do
field(:salmon, :string, default: nil)
field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
field(:pinned_activities, {:array, :string}, default: [])
field(:flavour, :string, default: nil)
field(:notification_settings, :map,
default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true}
)
# Found in the wild
# ap_id -> Where is this used?
# bio -> Where is this used?
@ -56,6 +63,19 @@ defmodule Pleroma.User.Info do
|> validate_required([:deactivated])
end
def update_notification_settings(info, settings) do
notification_settings =
info.notification_settings
|> Map.merge(settings)
|> Map.take(["remote", "local", "followers", "follows"])
params = %{notification_settings: notification_settings}
info
|> cast(params, [:notification_settings])
|> validate_required([:notification_settings])
end
def add_to_note_count(info, number) do
set_note_count(info, info.note_count + number)
end
@ -92,6 +112,14 @@ defmodule Pleroma.User.Info do
|> validate_required([:blocks])
end
def set_subscribers(info, subscribers) do
params = %{subscribers: subscribers}
info
|> cast(params, [:subscribers])
|> validate_required([:subscribers])
end
def add_to_mutes(info, muted) do
set_mutes(info, Enum.uniq([muted | info.mutes]))
end
@ -108,6 +136,14 @@ defmodule Pleroma.User.Info do
set_blocks(info, List.delete(info.blocks, blocked))
end
def add_to_subscribers(info, subscribed) do
set_subscribers(info, Enum.uniq([subscribed | info.subscribers]))
end
def remove_from_subscribers(info, subscribed) do
set_subscribers(info, List.delete(info.subscribers, subscribed))
end
def set_domain_blocks(info, domain_blocks) do
params = %{domain_blocks: domain_blocks}
@ -167,6 +203,7 @@ defmodule Pleroma.User.Info do
:banner,
:hide_follows,
:hide_followers,
:hide_favorites,
:background,
:show_role
])
@ -190,14 +227,6 @@ defmodule Pleroma.User.Info do
cast(info, params, [:confirmation_pending, :confirmation_token])
end
def mastodon_profile_update(info, params) do
info
|> cast(params, [
:locked,
:banner
])
end
def mastodon_settings_update(info, settings) do
params = %{settings: settings}
@ -259,4 +288,16 @@ defmodule Pleroma.User.Info do
moderator: is_moderator
}
end
def add_reblog_mute(info, ap_id) do
params = %{muted_reblogs: info.muted_reblogs ++ [ap_id]}
cast(info, params, [:muted_reblogs])
end
def remove_reblog_mute(info, ap_id) do
params = %{muted_reblogs: List.delete(info.muted_reblogs, ap_id)}
cast(info, params, [:muted_reblogs])
end
end

View file

@ -6,40 +6,119 @@ defmodule Pleroma.UserInviteToken do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Pleroma.Repo
alias Pleroma.UserInviteToken
@type t :: %__MODULE__{}
@type token :: String.t()
schema "user_invite_tokens" do
field(:token, :string)
field(:used, :boolean, default: false)
field(:max_use, :integer)
field(:expires_at, :date)
field(:uses, :integer, default: 0)
field(:invite_type, :string)
timestamps()
end
def create_token do
@spec create_invite(map()) :: UserInviteToken.t()
def create_invite(params \\ %{}) do
%UserInviteToken{}
|> cast(params, [:max_use, :expires_at])
|> add_token()
|> assign_type()
|> Repo.insert()
end
defp add_token(changeset) do
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
token = %UserInviteToken{
used: false,
token: token
}
Repo.insert(token)
put_change(changeset, :token, token)
end
def used_changeset(struct) do
struct
|> cast(%{}, [])
|> put_change(:used, true)
defp assign_type(%{changes: %{max_use: _max_use, expires_at: _expires_at}} = changeset) do
put_change(changeset, :invite_type, "reusable_date_limited")
end
def mark_as_used(token) do
with %{used: false} = token <- Repo.get_by(UserInviteToken, %{token: token}),
{:ok, token} <- Repo.update(used_changeset(token)) do
{:ok, token}
else
_e -> {:error, token}
defp assign_type(%{changes: %{expires_at: _expires_at}} = changeset) do
put_change(changeset, :invite_type, "date_limited")
end
defp assign_type(%{changes: %{max_use: _max_use}} = changeset) do
put_change(changeset, :invite_type, "reusable")
end
defp assign_type(changeset), do: put_change(changeset, :invite_type, "one_time")
@spec list_invites() :: [UserInviteToken.t()]
def list_invites do
query = from(u in UserInviteToken, order_by: u.id)
Repo.all(query)
end
@spec update_invite!(UserInviteToken.t(), map()) :: UserInviteToken.t() | no_return()
def update_invite!(invite, changes) do
change(invite, changes) |> Repo.update!()
end
@spec update_invite(UserInviteToken.t(), map()) ::
{:ok, UserInviteToken.t()} | {:error, Changeset.t()}
def update_invite(invite, changes) do
change(invite, changes) |> Repo.update()
end
@spec find_by_token!(token()) :: UserInviteToken.t() | no_return()
def find_by_token!(token), do: Repo.get_by!(UserInviteToken, token: token)
@spec find_by_token(token()) :: {:ok, UserInviteToken.t()} | nil
def find_by_token(token) do
with invite <- Repo.get_by(UserInviteToken, token: token) do
{:ok, invite}
end
end
@spec valid_invite?(UserInviteToken.t()) :: boolean()
def valid_invite?(%{invite_type: "one_time"} = invite) do
not invite.used
end
def valid_invite?(%{invite_type: "date_limited"} = invite) do
not_overdue_date?(invite) and not invite.used
end
def valid_invite?(%{invite_type: "reusable"} = invite) do
invite.uses < invite.max_use and not invite.used
end
def valid_invite?(%{invite_type: "reusable_date_limited"} = invite) do
not_overdue_date?(invite) and invite.uses < invite.max_use and not invite.used
end
defp not_overdue_date?(%{expires_at: expires_at}) do
Date.compare(Date.utc_today(), expires_at) in [:lt, :eq]
end
@spec update_usage!(UserInviteToken.t()) :: nil | UserInviteToken.t() | no_return()
def update_usage!(%{invite_type: "date_limited"}), do: nil
def update_usage!(%{invite_type: "one_time"} = invite),
do: update_invite!(invite, %{used: true})
def update_usage!(%{invite_type: invite_type} = invite)
when invite_type == "reusable" or invite_type == "reusable_date_limited" do
changes = %{
uses: invite.uses + 1
}
changes =
if changes.uses >= invite.max_use do
Map.put(changes, :used, true)
else
changes
end
update_invite!(invite, changes)
end
end

View file

@ -7,13 +7,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Instances
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.Upload
alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Federator
alias Pleroma.Web.OStatus
alias Pleroma.Web.WebFinger
import Ecto.Query
@ -89,15 +90,36 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
end
def insert(map, local \\ true) when is_map(map) do
def increase_replies_count_if_reply(%{
"object" => %{"inReplyTo" => reply_ap_id} = object,
"type" => "Create"
}) do
if is_public?(object) do
Object.increase_replies_count(reply_ap_id)
end
end
def increase_replies_count_if_reply(_create_data), do: :noop
def decrease_replies_count_if_reply(%Object{
data: %{"inReplyTo" => reply_ap_id} = object
}) do
if is_public?(object) do
Object.decrease_replies_count(reply_ap_id)
end
end
def decrease_replies_count_if_reply(_object), do: :noop
def insert(map, local \\ true, fake \\ false) when is_map(map) do
with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map),
map <- lazy_put_activity_defaults(map, fake),
:ok <- check_actor_is_active(map["actor"]),
{_, true} <- {:remote_limit_error, check_remote_limit(map)},
{:ok, map} <- MRF.filter(map),
:ok <- insert_full_object(map) do
{recipients, _, _} = get_recipients(map)
{recipients, _, _} = get_recipients(map),
{:fake, false, map, recipients} <- {:fake, fake, map, recipients},
{:ok, map, object} <- insert_full_object(map) do
{:ok, activity} =
Repo.insert(%Activity{
data: map,
@ -106,6 +128,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
recipients: recipients
})
# Splice in the child object if we have one.
activity =
if !is_nil(object) do
Map.put(activity, :object, object)
else
activity
end
Task.start(fn ->
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
end)
@ -114,8 +144,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
stream_out(activity)
{:ok, activity}
else
%Activity{} = activity -> {:ok, activity}
error -> {:error, error}
%Activity{} = activity ->
{:ok, activity}
{:fake, true, map, recipients} ->
activity = %Activity{
data: map,
local: local,
actor: map["actor"],
recipients: recipients,
id: "pleroma:fakeid"
}
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
{:ok, activity}
error ->
{:error, error}
end
end
@ -134,12 +179,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
if activity.data["type"] in ["Create"] do
activity.data["object"]
object = Object.normalize(activity)
object.data
|> Map.get("tag", [])
|> Enum.filter(fn tag -> is_bitstring(tag) end)
|> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
if activity.data["object"]["attachment"] != [] do
if object.data["attachment"] != [] do
Pleroma.Web.Streamer.stream("public:media", activity)
if activity.local do
@ -151,14 +198,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
if !Enum.member?(activity.data["cc"] || [], public) &&
!Enum.member?(
activity.data["to"],
User.get_by_ap_id(activity.data["actor"]).follower_address
User.get_cached_by_ap_id(activity.data["actor"]).follower_address
),
do: Pleroma.Web.Streamer.stream("direct", activity)
end
end
end
def create(%{to: to, actor: actor, context: context, object: object} = params) do
def create(%{to: to, actor: actor, context: context, object: object} = params, fake \\ false) do
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
@ -169,12 +216,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
%{to: to, actor: actor, published: published, context: context, object: object},
additional
),
{:ok, activity} <- insert(create_data, local),
{:ok, activity} <- insert(create_data, local, fake),
{:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data),
# Changing note count prior to enqueuing federation task in order to avoid
# race conditions on updating user.info
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
{:fake, true, activity} ->
{:ok, activity}
end
end
@ -321,6 +373,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
"deleted_activity_id" => activity && activity.id
},
{:ok, activity} <- insert(data, local),
_ <- decrease_replies_count_if_reply(object),
# Changing note count prior to enqueuing federation task in order to avoid
# race conditions on updating user.info
{:ok, _actor} <- decrease_note_count_if_public(user, object),
@ -370,20 +423,38 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
content: content
} = params
) do
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
forward = !(params[:forward] == false)
%{
additional = params[:additional] || %{}
params = %{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content
}
|> make_flag_data(additional)
|> insert(local)
additional =
if forward do
Map.merge(additional, %{"to" => [], "cc" => [account.ap_id]})
else
Map.merge(additional, %{"to" => [], "cc" => []})
end
with flag_data <- make_flag_data(params, additional),
{:ok, activity} <- insert(flag_data, local),
:ok <- maybe_federate(activity) do
Enum.each(User.all_superusers(), fn superuser ->
superuser
|> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content)
|> Pleroma.Emails.Mailer.deliver_async()
end)
{:ok, activity}
end
end
def fetch_activities_for_context(context, opts \\ %{}) do
@ -412,6 +483,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
),
order_by: [desc: :id]
)
|> Activity.with_preloaded_object()
Repo.all(query)
end
@ -421,7 +493,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
q
|> restrict_unlisted()
|> Repo.all()
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
end
@ -500,37 +572,49 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_since(query, _), do: query
defp restrict_tag_reject(_query, %{"tag_reject" => _tag_reject, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
when is_list(tag_reject) and tag_reject != [] do
from(
activity in query,
where: fragment(~s(\(not \(? #> '{"object","tag"}'\) \\?| ?\)), activity.data, ^tag_reject)
[_activity, object] in query,
where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject)
)
end
defp restrict_tag_reject(query, _), do: query
defp restrict_tag_all(_query, %{"tag_all" => _tag_all, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_tag_all(query, %{"tag_all" => tag_all})
when is_list(tag_all) and tag_all != [] do
from(
activity in query,
where: fragment(~s(\(? #> '{"object","tag"}'\) \\?& ?), activity.data, ^tag_all)
[_activity, object] in query,
where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all)
)
end
defp restrict_tag_all(query, _), do: query
defp restrict_tag(_query, %{"tag" => _tag, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do
from(
activity in query,
where: fragment(~s(\(? #> '{"object","tag"}'\) \\?| ?), activity.data, ^tag)
[_activity, object] in query,
where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag)
)
end
defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
from(
activity in query,
where: fragment(~s(? <@ (? #> '{"object","tag"}'\)), ^tag, activity.data)
[_activity, object] in query,
where: fragment("(?)->'tag' \\? (?)", object.data, ^tag)
)
end
@ -564,26 +648,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
)
end
defp restrict_limit(query, %{"limit" => limit}) do
from(activity in query, limit: ^limit)
end
defp restrict_limit(query, _), do: query
defp restrict_local(query, %{"local_only" => true}) do
from(activity in query, where: activity.local == true)
end
defp restrict_local(query, _), do: query
defp restrict_max(query, %{"max_id" => ""}), do: query
defp restrict_max(query, %{"max_id" => max_id}) do
from(activity in query, where: activity.id < ^max_id)
end
defp restrict_max(query, _), do: query
defp restrict_actor(query, %{"actor_id" => actor_id}) do
from(activity in query, where: activity.actor == ^actor_id)
end
@ -609,10 +679,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_favorited_by(query, _), do: query
defp restrict_media(_query, %{"only_media" => _val, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
from(
activity in query,
where: fragment(~s(not (? #> '{"object","attachment"}' = ?\)), activity.data, ^[])
[_activity, object] in query,
where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
)
end
@ -654,7 +728,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
from(
activity in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocks),
where: fragment("not (?->'to' \\?| ?)", activity.data, ^blocks),
where: fragment("not (? && ?)", activity.recipients, ^blocks),
where:
fragment(
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
activity.data,
activity.data,
^blocks
),
where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks)
)
end
@ -679,23 +760,41 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_pinned(query, _), do: query
defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do
muted_reblogs = info.muted_reblogs || []
from(
activity in query,
where:
fragment(
"not ( ?->>'type' = 'Announce' and ? = ANY(?))",
activity.data,
activity.actor,
^muted_reblogs
)
)
end
defp restrict_muted_reblogs(query, _), do: query
defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
defp maybe_preload_objects(query, _) do
query
|> Activity.with_preloaded_object()
end
def fetch_activities_query(recipients, opts \\ %{}) do
base_query =
from(
activity in Activity,
limit: 20,
order_by: [fragment("? desc nulls last", activity.id)]
)
base_query = from(activity in Activity)
base_query
|> maybe_preload_objects(opts)
|> restrict_recipients(recipients, opts["user"])
|> restrict_tag(opts)
|> restrict_tag_reject(opts)
|> restrict_tag_all(opts)
|> restrict_since(opts)
|> restrict_local(opts)
|> restrict_limit(opts)
|> restrict_max(opts)
|> restrict_actor(opts)
|> restrict_type(opts)
|> restrict_favorited_by(opts)
@ -706,18 +805,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_replies(opts)
|> restrict_reblogs(opts)
|> restrict_pinned(opts)
|> restrict_muted_reblogs(opts)
end
def fetch_activities(recipients, opts \\ %{}) do
fetch_activities_query(recipients, opts)
|> Repo.all()
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
end
def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do
fetch_activities_query([], opts)
|> restrict_to_cc(recipients_to, recipients_cc)
|> Repo.all()
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
end
@ -782,7 +882,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def fetch_and_prepare_user_from_ap_id(ap_id) do
with {:ok, data} <- fetch_and_contain_remote_object_from_id(ap_id) do
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do
user_data_from_user_object(data)
else
e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
@ -790,7 +890,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def make_user_from_ap_id(ap_id) do
if _user = User.get_by_ap_id(ap_id) do
if _user = User.get_cached_by_ap_id(ap_id) do
Transmogrifier.upgrade_user_from_ap_id(ap_id)
else
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
@ -892,60 +992,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
# TODO:
# This will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id) do
if object = Object.get_cached_by_ap_id(id) do
{:ok, object}
else
with {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
nil <- Object.normalize(data),
params <- %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => data["actor"] || data["attributedTo"],
"object" => data
},
:ok <- Transmogrifier.contain_origin(id, params),
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, Object.normalize(activity.data["object"])}
else
{:error, {:reject, nil}} ->
{:reject, nil}
object = %Object{} ->
{:ok, object}
_e ->
Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.normalize(activity.data["object"])}
e -> e
end
end
end
end
def fetch_and_contain_remote_object_from_id(id) do
Logger.info("Fetching object #{id} via AP")
with true <- String.starts_with?(id, "http"),
{:ok, %{body: body, status: code}} when code in 200..299 <-
@httpoison.get(
id,
[{:Accept, "application/activity+json"}]
),
{:ok, data} <- Jason.decode(body),
:ok <- Transmogrifier.contain_origin_from_id(id, data) do
{:ok, data}
else
e ->
{:error, e}
end
end
# filter out broken threads
def contain_broken_threads(%Activity{} = activity, %User{} = user) do
entire_thread_visible_for_user?(activity, user)

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.ObjectView
@ -153,9 +154,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
true <- Utils.recipient_in_message(user.ap_id, params),
params <- Utils.maybe_splice_recipient(user.ap_id, params) do
with %User{} = recipient <- User.get_cached_by_nickname(nickname),
%User{} = actor <- User.get_or_fetch_by_ap_id(params["actor"]),
true <- Utils.recipient_in_message(recipient, actor, params),
params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
Federator.incoming_ap_doc(params)
json(conn, "ok")
end
@ -172,7 +174,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
"Signature missing or not from author, relayed Create message, fetching object from source"
)
ActivityPub.fetch_object_from_id(params["object"]["id"])
Fetcher.fetch_object_from_id(params["object"]["id"])
json(conn, "ok")
end

View file

@ -4,6 +4,10 @@
defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF
defp string_matches?(string, _) when not is_binary(string) do
false
end
defp string_matches?(string, pattern) when is_binary(pattern) do
String.contains?(string, pattern)
end
@ -44,6 +48,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
end
defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do
content =
if is_binary(content) do
content
else
""
end
summary =
if is_binary(summary) do
summary
else
""
end
{content, summary} =
Enum.reduce(
Pleroma.Config.get([:mrf_keyword, :replace]),
@ -60,11 +78,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|> put_in(["object", "summary"], summary)}
end
@impl true
def filter(%{"object" => %{"content" => nil}} = message) do
{:ok, message}
end
@impl true
def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message) do
with {:ok, message} <- check_reject(message),

View file

@ -41,7 +41,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do
def publish(%Activity{data: %{"type" => "Create"}} = activity) do
with %User{} = user <- get_actor(),
%Object{} = object <- Object.normalize(activity.data["object"]["id"]) do
%Object{} = object <- Object.normalize(activity) do
ActivityPub.announce(user, object, nil, true, false)
else
e -> Logger.error("error: #{inspect(e)}")

View file

@ -8,8 +8,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"""
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
@ -18,56 +20,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
require Logger
def get_actor(%{"actor" => actor}) when is_binary(actor) do
actor
end
def get_actor(%{"actor" => actor}) when is_list(actor) do
if is_binary(Enum.at(actor, 0)) do
Enum.at(actor, 0)
else
Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end)
|> Map.get("id")
end
end
def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do
id
end
def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
get_actor(%{"actor" => actor})
end
@doc """
Checks that an imported AP object's actor matches the domain it came from.
"""
def contain_origin(_id, %{"actor" => nil}), do: :error
def contain_origin(id, %{"actor" => _actor} = params) do
id_uri = URI.parse(id)
actor_uri = URI.parse(get_actor(params))
if id_uri.host == actor_uri.host do
:ok
else
:error
end
end
def contain_origin_from_id(_id, %{"id" => nil}), do: :error
def contain_origin_from_id(id, %{"id" => other_id} = _params) do
id_uri = URI.parse(id)
other_uri = URI.parse(other_id)
if id_uri.host == other_uri.host do
:ok
else
:error
end
end
@doc """
Modifies an incoming AP object (mastodon format) to our internal format.
"""
@ -83,14 +35,34 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> fix_content_map
|> fix_likes
|> fix_addressing
|> fix_summary
end
def fix_summary(%{"summary" => nil} = object) do
object
|> Map.put("summary", "")
end
def fix_summary(%{"summary" => _} = object) do
# summary is present, nothing to do
object
end
def fix_summary(object) do
object
|> Map.put("summary", "")
end
def fix_addressing_list(map, field) do
if is_binary(map[field]) do
map
|> Map.put(field, [map[field]])
else
map
cond do
is_binary(map[field]) ->
Map.put(map, field, [map[field]])
is_nil(map[field]) ->
Map.put(map, field, [])
true ->
map
end
end
@ -128,18 +100,47 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> fix_explicit_addressing(explicit_mentions)
end
# if as:Public is addressed, then make sure the followers collection is also addressed
# so that the activities will be delivered to local users.
def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
recipients = to ++ cc
if followers_collection not in recipients do
cond do
"https://www.w3.org/ns/activitystreams#Public" in cc ->
to = to ++ [followers_collection]
Map.put(object, "to", to)
"https://www.w3.org/ns/activitystreams#Public" in to ->
cc = cc ++ [followers_collection]
Map.put(object, "cc", cc)
true ->
object
end
else
object
end
end
def fix_implicit_addressing(object, _), do: object
def fix_addressing(object) do
%User{} = user = User.get_or_fetch_by_ap_id(object["actor"])
followers_collection = User.ap_followers(user)
object
|> fix_addressing_list("to")
|> fix_addressing_list("cc")
|> fix_addressing_list("bto")
|> fix_addressing_list("bcc")
|> fix_explicit_addressing
|> fix_implicit_addressing(followers_collection)
end
def fix_actor(%{"attributedTo" => actor} = object) do
object
|> Map.put("actor", get_actor(%{"actor" => actor}))
|> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
end
# Check for standardisation
@ -174,14 +175,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
""
end
case fetch_obj_helper(in_reply_to_id) do
case get_obj_helper(in_reply_to_id) do
{:ok, replied_object} ->
with %Activity{} = activity <-
with %Activity{} = _activity <-
Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
|> Map.put("inReplyToStatusId", activity.id)
|> Map.put("conversation", replied_object.data["context"] || object["conversation"])
|> Map.put("context", replied_object.data["context"] || object["conversation"])
else
@ -355,6 +355,40 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
# with nil ID.
def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data) do
with context <- data["context"] || Utils.generate_context_id(),
content <- data["content"] || "",
%User{} = actor <- User.get_cached_by_ap_id(actor),
# Reduce the object list to find the reported user.
%User{} = account <-
Enum.reduce_while(objects, nil, fn ap_id, _ ->
with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
{:halt, user}
else
_ -> {:cont, nil}
end
end),
# Remove the reported user from the object list.
statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
params = %{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content,
additional: %{
"cc" => [account.ap_id]
}
}
ActivityPub.flag(params)
end
end
# disallow objects with bogus IDs
def handle_incoming(%{"id" => nil}), do: :error
def handle_incoming(%{"id" => ""}), do: :error
@ -366,7 +400,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
# - emoji
def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
when objtype in ["Article", "Note", "Video", "Page"] do
actor = get_actor(data)
actor = Containment.get_actor(data)
data =
Map.put(data, "actor", actor)
@ -404,27 +438,53 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
if not User.locked?(followed) do
with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
{:user_blocked, false} <-
{:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
{:user_locked, false} <- {:user_locked, User.locked?(followed)},
{:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed,
object: data,
local: true
})
else
{:user_blocked, true} ->
{:ok, _} = Utils.update_follow_state(activity, "reject")
User.follow(follower, followed)
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed,
object: data,
local: true
})
{:follow, {:error, _}} ->
{:ok, _} = Utils.update_follow_state(activity, "reject")
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed,
object: data,
local: true
})
{:user_locked, true} ->
:noop
end
{:ok, activity}
else
_e -> :error
_e ->
:error
end
end
def handle_incoming(
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
) do
with actor <- get_actor(data),
with actor <- Containment.get_actor(data),
{:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
@ -450,7 +510,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
) do
with actor <- get_actor(data),
with actor <- Containment.get_actor(data),
{:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
@ -474,9 +534,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
) do
with actor <- get_actor(data),
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
{:ok, activity}
else
@ -487,9 +547,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
) do
with actor <- get_actor(data),
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
{:ok, object} <- get_obj_helper(object_id),
public <- Visibility.is_public?(data),
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
{:ok, activity}
@ -503,7 +563,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
data
)
when object_type in ["Person", "Application", "Service", "Organization"] do
with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do
with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
banner = new_user_data[:info]["banner"]
@ -542,10 +602,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
) do
object_id = Utils.get_ap_id(object_id)
with actor <- get_actor(data),
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
:ok <- contain_origin(actor.ap_id, object.data),
{:ok, object} <- get_obj_helper(object_id),
:ok <- Containment.contain_origin(actor.ap_id, object.data),
{:ok, activity} <- ActivityPub.delete(object, false) do
{:ok, activity}
else
@ -561,9 +621,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"id" => id
} = data
) do
with actor <- get_actor(data),
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
{:ok, activity}
else
@ -631,9 +691,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"id" => id
} = data
) do
with actor <- get_actor(data),
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
{:ok, activity}
else
@ -643,9 +703,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(_), do: :error
def fetch_obj_helper(id) when is_bitstring(id), do: ActivityPub.fetch_object_from_id(id)
def fetch_obj_helper(obj) when is_map(obj), do: ActivityPub.fetch_object_from_id(obj["id"])
def get_obj_helper(id) do
if object = Object.normalize(id), do: {:ok, object}, else: nil
end
@ -682,9 +739,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
# internal -> Mastodon
# """
def prepare_outgoing(%{"type" => "Create", "object" => object} = data) do
def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
object =
object
Object.normalize(object_id).data
|> prepare_object
data =
@ -745,7 +802,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def maybe_fix_object_url(data) do
if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
case fetch_obj_helper(data["object"]) do
case get_obj_helper(data["object"]) do
{:ok, relative_object} ->
if relative_object.data["external_url"] do
_data =
@ -887,8 +944,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
defp strip_internal_tags(object), do: object
defp user_upgrade_task(user) do
old_follower_address = User.ap_followers(user)
def perform(:user_upgrade, user) do
# we pass a fake user so that the followers collection is stripped away
old_follower_address = User.ap_followers(%User{nickname: user.nickname})
q =
from(
@ -931,28 +989,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Repo.update_all(q, [])
end
def upgrade_user_from_ap_id(ap_id, async \\ true) do
with %User{local: false} = user <- User.get_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
already_ap = User.ap_enabled?(user)
{:ok, user} =
User.upgrade_changeset(user, data)
|> Repo.update()
if !already_ap do
# This could potentially take a long time, do it in the background
if async do
Task.start(fn ->
user_upgrade_task(user)
end)
else
user_upgrade_task(user)
end
def upgrade_user_from_ap_id(ap_id) do
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
already_ap <- User.ap_enabled?(user),
{:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
unless already_ap do
PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
end
{:ok, user}
else
%User{} = user -> {:ok, user}
e -> e
end
end

View file

@ -52,7 +52,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
defp recipient_in_collection(ap_id, coll) when is_list(coll), do: ap_id in coll
defp recipient_in_collection(_, _), do: false
def recipient_in_message(ap_id, params) do
def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params) do
cond do
recipient_in_collection(ap_id, params["to"]) ->
true
@ -71,6 +71,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do
!params["to"] && !params["cc"] && !params["bto"] && !params["bcc"] ->
true
# if the message is sent from somebody the user is following, then assume it
# is addressed to the recipient
User.following?(recipient, actor) ->
true
true ->
false
end
@ -99,7 +104,10 @@ defmodule Pleroma.Web.ActivityPub.Utils do
%{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"#{Web.base_url()}/schemas/litepub-0.1.jsonld"
"#{Web.base_url()}/schemas/litepub-0.1.jsonld",
%{
"@language" => "und"
}
]
}
end
@ -175,18 +183,26 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Adds an id and a published data if they aren't there,
also adds it to an included object
"""
def lazy_put_activity_defaults(map) do
%{data: %{"id" => context}, id: context_id} = create_context(map["context"])
def lazy_put_activity_defaults(map, fake \\ false) do
map =
map
|> Map.put_new_lazy("id", &generate_activity_id/0)
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", context)
|> Map.put_new("context_id", context_id)
unless fake do
%{data: %{"id" => context}, id: context_id} = create_context(map["context"])
map
|> Map.put_new_lazy("id", &generate_activity_id/0)
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", context)
|> Map.put_new("context_id", context_id)
else
map
|> Map.put_new("id", "pleroma:fakeid")
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", "pleroma:fakecontext")
|> Map.put_new("context_id", -1)
end
if is_map(map["object"]) do
object = lazy_put_object_defaults(map["object"], map)
object = lazy_put_object_defaults(map["object"], map, fake)
%{map | "object" => object}
else
map
@ -196,7 +212,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do
@doc """
Adds an id and published date if they aren't there.
"""
def lazy_put_object_defaults(map, activity \\ %{}) do
def lazy_put_object_defaults(map, activity \\ %{}, fake)
def lazy_put_object_defaults(map, activity, true = _fake) do
map
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("id", "pleroma:fake_object_id")
|> Map.put_new("context", activity["context"])
|> Map.put_new("fake", true)
|> Map.put_new("context_id", activity["context_id"])
end
def lazy_put_object_defaults(map, activity, _fake) do
map
|> Map.put_new_lazy("id", &generate_object_id/0)
|> Map.put_new_lazy("published", &make_date/0)
@ -207,14 +234,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do
@doc """
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => type} = object_data})
def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
when is_map(object_data) and type in @supported_object_types do
with {:ok, _} <- Object.create(object_data) do
:ok
with {:ok, object} <- Object.create(object_data) do
map =
map
|> Map.put("object", object.data["id"])
{:ok, map, object}
end
end
def insert_full_object(_), do: :ok
def insert_full_object(map), do: {:ok, map, nil}
def update_object_in_activities(%{data: %{"id" => id}} = object) do
# TODO
@ -354,7 +385,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
[state, actor, object]
)
activity = Repo.get(Activity, activity.id)
activity = Activity.get_by_id(activity.id)
{:ok, activity}
rescue
e ->
@ -404,13 +435,15 @@ defmodule Pleroma.Web.ActivityPub.Utils do
activity.data
),
where: activity.actor == ^follower_id,
# this is to use the index
where:
fragment(
"? @> ?",
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
^%{object: followed_id}
activity.data,
^followed_id
),
order_by: [desc: :id],
order_by: [fragment("? desc nulls last", activity.id)],
limit: 1
)
@ -567,13 +600,15 @@ defmodule Pleroma.Web.ActivityPub.Utils do
activity.data
),
where: activity.actor == ^blocker_id,
# this is to use the index
where:
fragment(
"? @> ?",
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
^%{object: blocked_id}
activity.data,
^blocked_id
),
order_by: [desc: :id],
order_by: [fragment("? desc nulls last", activity.id)],
limit: 1
)
@ -621,7 +656,13 @@ defmodule Pleroma.Web.ActivityPub.Utils do
#### Flag-related helpers
def make_flag_data(params, additional) do
status_ap_ids = Enum.map(params.statuses || [], & &1.data["id"])
status_ap_ids =
Enum.map(params.statuses || [], fn
%Activity{} = act -> act.data["id"]
act when is_map(act) -> act["id"]
act when is_binary(act) -> act
end)
object = [params.account.ap_id] ++ status_ap_ids
%{

View file

@ -17,7 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
def render("object.json", %{object: %Activity{data: %{"type" => "Create"}} = activity}) do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
object = Object.normalize(activity.data["object"])
object = Object.normalize(activity)
additional =
Transmogrifier.prepare_object(activity.data)
@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
def render("object.json", %{object: %Activity{} = activity}) do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
object = Object.normalize(activity.data["object"])
object = Object.normalize(activity)
additional =
Transmogrifier.prepare_object(activity.data)

View file

@ -87,16 +87,10 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"publicKeyPem" => public_key
},
"endpoints" => endpoints,
"icon" => %{
"type" => "Image",
"url" => User.avatar_url(user)
},
"image" => %{
"type" => "Image",
"url" => User.banner_url(user)
},
"tag" => user.info.source_data["tag"] || []
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
|> Map.merge(Utils.make_json_ld_header())
end
@ -294,4 +288,17 @@ defmodule Pleroma.Web.ActivityPub.UserView do
map
end
end
defp maybe_make_image(func, key, user) do
if image = func.(user, no_default: true) do
%{
key => %{
"type" => "Image",
"url" => image
}
}
else
%{}
end
end
end

View file

@ -41,16 +41,21 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
# guard
def entire_thread_visible_for_user?(nil, _user), do: false
# child
def entire_thread_visible_for_user?(
%Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail,
user
)
when is_binary(parent_id) do
parent = Activity.get_in_reply_to_activity(tail)
visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user)
end
# XXX: Probably even more inefficient than the previous implementation intended to be a placeholder untill https://git.pleroma.social/pleroma/pleroma/merge_requests/971 is in develop
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
# root
def entire_thread_visible_for_user?(tail, user), do: visible_for_user?(tail, user)
def entire_thread_visible_for_user?(
%Activity{} = tail,
# %Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail,
user
) do
case Object.normalize(tail) do
%{data: %{"inReplyTo" => parent_id}} when is_binary(parent_id) ->
parent = Activity.get_in_reply_to_activity(tail)
visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user)
_ ->
visible_for_user?(tail, user)
end
end
end

View file

@ -3,27 +3,49 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AdminAPIController do
@users_page_size 50
use Pleroma.Web, :controller
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.Search
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
require Logger
@users_page_size 50
action_fallback(:errors)
def user_delete(conn, %{"nickname" => nickname}) do
User.get_by_nickname(nickname)
User.get_cached_by_nickname(nickname)
|> User.delete()
conn
|> json(nickname)
end
def user_follow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do
with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.follow(follower, followed)
end
conn
|> json("ok")
end
def user_unfollow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do
with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.unfollow(follower, followed)
end
conn
|> json("ok")
end
def user_create(
conn,
%{"nickname" => nickname, "email" => email, "password" => password}
@ -44,8 +66,17 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> json(user.nickname)
end
def user_show(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
conn
|> json(AccountView.render("show.json", %{user: user}))
else
_ -> {:error, :not_found}
end
end
def user_toggle_activation(conn, %{"nickname" => nickname}) do
user = User.get_by_nickname(nickname)
user = User.get_cached_by_nickname(nickname)
{:ok, updated_user} = User.deactivate(user, !user.info.deactivated)
@ -63,17 +94,17 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
do: json_response(conn, :no_content, "")
end
def list_users(%{assigns: %{user: admin}} = conn, params) do
def list_users(conn, params) do
{page, page_size} = page_params(params)
filters = maybe_parse_filters(params["filters"])
with {:ok, users, count} <-
User.search_for_admin(%{
query: params["query"],
admin: admin,
local: params["local_only"] == "true",
page: page,
page_size: page_size
}),
search_params = %{
query: params["query"],
page: page,
page_size: page_size
}
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
do:
conn
|> json(
@ -85,9 +116,22 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
)
end
@filters ~w(local external active deactivated)
defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
@spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
defp maybe_parse_filters(filters) do
filters
|> String.split(",")
|> Enum.filter(&Enum.member?(@filters, &1))
|> Enum.map(&String.to_atom(&1))
|> Enum.into(%{}, &{&1, true})
end
def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname})
when permission_group in ["moderator", "admin"] do
user = User.get_by_nickname(nickname)
user = User.get_cached_by_nickname(nickname)
info =
%{}
@ -112,7 +156,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
def right_get(conn, %{"nickname" => nickname}) do
user = User.get_by_nickname(nickname)
user = User.get_cached_by_nickname(nickname)
conn
|> json(%{
@ -134,7 +178,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> put_status(403)
|> json(%{error: "You can't revoke your own admin status."})
else
user = User.get_by_nickname(nickname)
user = User.get_cached_by_nickname(nickname)
info =
%{}
@ -160,7 +204,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do
with {:ok, status} <- Ecto.Type.cast(:boolean, status),
%User{} = user <- User.get_by_nickname(nickname),
%User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, _} <- User.deactivate(user, !status),
do: json_response(conn, :no_content, "")
end
@ -192,29 +236,58 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
with true <-
Pleroma.Config.get([:instance, :invites_enabled]) &&
!Pleroma.Config.get([:instance, :registrations_open]),
{:ok, invite_token} <- Pleroma.UserInviteToken.create_token(),
{:ok, invite_token} <- UserInviteToken.create_invite(),
email <-
Pleroma.UserEmail.user_invitation_email(user, invite_token, email, params["name"]),
{:ok, _} <- Pleroma.Mailer.deliver(email) do
Pleroma.Emails.UserEmail.user_invitation_email(
user,
invite_token,
email,
params["name"]
),
{:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
json_response(conn, :no_content, "")
end
end
@doc "Get a account registeration invite token (base64 string)"
def get_invite_token(conn, _params) do
{:ok, token} = Pleroma.UserInviteToken.create_token()
def get_invite_token(conn, params) do
options = params["invite"] || %{}
{:ok, invite} = UserInviteToken.create_invite(options)
conn
|> json(invite.token)
end
@doc "Get list of created invites"
def invites(conn, _params) do
invites = UserInviteToken.list_invites()
conn
|> json(AccountView.render("invites.json", %{invites: invites}))
end
@doc "Revokes invite by token"
def revoke_invite(conn, %{"token" => token}) do
invite = UserInviteToken.find_by_token!(token)
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true})
conn
|> json(AccountView.render("invite.json", %{invite: updated_invite}))
end
@doc "Get a password reset token (base64 string) for given nickname"
def get_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_cached_by_nickname(nickname)
{:ok, token} = Pleroma.PasswordResetToken.create_token(user)
conn
|> json(token.token)
end
@doc "Get a password reset token (base64 string) for given nickname"
def get_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_by_nickname(nickname)
{:ok, token} = Pleroma.PasswordResetToken.create_token(user)
def errors(conn, {:error, :not_found}) do
conn
|> json(token.token)
|> put_status(404)
|> json("Not found")
end
def errors(conn, {:param_cast, _}) do

View file

@ -0,0 +1,54 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.Search do
import Ecto.Query
alias Pleroma.Repo
alias Pleroma.User
@page_size 50
def user(%{query: term} = params) when is_nil(term) or term == "" do
query = maybe_filtered_query(params)
paginated_query =
maybe_filtered_query(params)
|> paginate(params[:page] || 1, params[:page_size] || @page_size)
count = query |> Repo.aggregate(:count, :id)
results = Repo.all(paginated_query)
{:ok, results, count}
end
def user(%{query: term} = params) when is_binary(term) do
search_query = from(u in maybe_filtered_query(params), where: ilike(u.nickname, ^"%#{term}%"))
count = search_query |> Repo.aggregate(:count, :id)
results =
search_query
|> paginate(params[:page] || 1, params[:page_size] || @page_size)
|> Repo.all()
{:ok, results, count}
end
defp maybe_filtered_query(params) do
from(u in User, order_by: u.nickname)
|> User.maybe_local_user_query(params[:local])
|> User.maybe_external_user_query(params[:external])
|> User.maybe_active_user_query(params[:active])
|> User.maybe_deactivated_user_query(params[:deactivated])
end
defp paginate(query, page, page_size) do
from(u in query,
limit: ^page_size,
offset: ^((page - 1) * page_size)
)
end
end

View file

@ -26,4 +26,22 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
"tags" => user.tags || []
}
end
def render("invite.json", %{invite: invite}) do
%{
"id" => invite.id,
"token" => invite.token,
"used" => invite.used,
"expires_at" => invite.expires_at,
"uses" => invite.uses,
"max_use" => invite.max_use,
"invite_type" => invite.invite_type
}
end
def render("invites.json", %{invites: invites}) do
%{
invites: render_many(invites, AccountView, "invite.json", as: :invite)
}
end
end

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.Authenticator do
alias Pleroma.Registration
alias Pleroma.User
def implementation do
@ -15,11 +16,30 @@ defmodule Pleroma.Web.Auth.Authenticator do
@callback get_user(Plug.Conn.t()) :: {:ok, User.t()} | {:error, any()}
def get_user(plug), do: implementation().get_user(plug)
@callback create_from_registration(Plug.Conn.t(), Registration.t()) ::
{:ok, User.t()} | {:error, any()}
def create_from_registration(plug, registration),
do: implementation().create_from_registration(plug, registration)
@callback get_registration(Plug.Conn.t()) ::
{:ok, Registration.t()} | {:error, any()}
def get_registration(plug), do: implementation().get_registration(plug)
@callback handle_error(Plug.Conn.t(), any()) :: any()
def handle_error(plug, error), do: implementation().handle_error(plug, error)
def handle_error(plug, error),
do: implementation().handle_error(plug, error)
@callback auth_template() :: String.t() | nil
def auth_template do
implementation().auth_template() || Pleroma.Config.get(:auth_template, "show.html")
# Note: `config :pleroma, :auth_template, "..."` support is deprecated
implementation().auth_template() ||
Pleroma.Config.get([:auth, :auth_template], Pleroma.Config.get(:auth_template)) ||
"show.html"
end
@callback oauth_consumer_template() :: String.t() | nil
def oauth_consumer_template do
implementation().oauth_consumer_template() ||
Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html")
end
end

View file

@ -0,0 +1,144 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.LDAPAuthenticator do
alias Pleroma.User
require Logger
@behaviour Pleroma.Web.Auth.Authenticator
@base Pleroma.Web.Auth.PleromaAuthenticator
@connection_timeout 10_000
@search_timeout 10_000
defdelegate get_registration(conn), to: @base
defdelegate create_from_registration(conn, registration), to: @base
defdelegate handle_error(conn, error), to: @base
defdelegate auth_template, to: @base
defdelegate oauth_consumer_template, to: @base
def get_user(%Plug.Conn{} = conn) do
if Pleroma.Config.get([:ldap, :enabled]) do
{name, password} =
case conn.params do
%{"authorization" => %{"name" => name, "password" => password}} ->
{name, password}
%{"grant_type" => "password", "username" => name, "password" => password} ->
{name, password}
end
case ldap_user(name, password) do
%User{} = user ->
{:ok, user}
{:error, {:ldap_connection_error, _}} ->
# When LDAP is unavailable, try default authenticator
@base.get_user(conn)
error ->
error
end
else
# Fall back to default authenticator
@base.get_user(conn)
end
end
defp ldap_user(name, password) do
ldap = Pleroma.Config.get(:ldap, [])
host = Keyword.get(ldap, :host, "localhost")
port = Keyword.get(ldap, :port, 389)
ssl = Keyword.get(ldap, :ssl, false)
sslopts = Keyword.get(ldap, :sslopts, [])
options =
[{:port, port}, {:ssl, ssl}, {:timeout, @connection_timeout}] ++
if sslopts != [], do: [{:sslopts, sslopts}], else: []
case :eldap.open([to_charlist(host)], options) do
{:ok, connection} ->
try do
if Keyword.get(ldap, :tls, false) do
:application.ensure_all_started(:ssl)
case :eldap.start_tls(
connection,
Keyword.get(ldap, :tlsopts, []),
@connection_timeout
) do
:ok ->
:ok
error ->
Logger.error("Could not start TLS: #{inspect(error)}")
end
end
bind_user(connection, ldap, name, password)
after
:eldap.close(connection)
end
{:error, error} ->
Logger.error("Could not open LDAP connection: #{inspect(error)}")
{:error, {:ldap_connection_error, error}}
end
end
defp bind_user(connection, ldap, name, password) do
uid = Keyword.get(ldap, :uid, "cn")
base = Keyword.get(ldap, :base)
case :eldap.simple_bind(connection, "#{uid}=#{name},#{base}", password) do
:ok ->
case User.get_by_nickname_or_email(name) do
%User{} = user ->
user
_ ->
register_user(connection, base, uid, name, password)
end
error ->
error
end
end
defp register_user(connection, base, uid, name, password) do
case :eldap.search(connection, [
{:base, to_charlist(base)},
{:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))},
{:scope, :eldap.wholeSubtree()},
{:attributes, ['mail', 'email']},
{:timeout, @search_timeout}
]) do
{:ok, {:eldap_search_result, [{:eldap_entry, _, attributes}], _}} ->
with {_, [mail]} <- List.keyfind(attributes, 'mail', 0) do
params = %{
email: :erlang.list_to_binary(mail),
name: name,
nickname: name,
password: password,
password_confirmation: password
}
changeset = User.register_changeset(%User{}, params)
case User.register(changeset) do
{:ok, user} -> user
error -> error
end
else
_ ->
Logger.error("Could not find LDAP attribute mail: #{inspect(attributes)}")
{:error, :ldap_registration_missing_attributes}
end
error ->
error
end
end
end

View file

@ -4,12 +4,21 @@
defmodule Pleroma.Web.Auth.PleromaAuthenticator do
alias Comeonin.Pbkdf2
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
@behaviour Pleroma.Web.Auth.Authenticator
def get_user(%Plug.Conn{} = conn) do
%{"authorization" => %{"name" => name, "password" => password}} = conn.params
{name, password} =
case conn.params do
%{"authorization" => %{"name" => name, "password" => password}} ->
{name, password}
%{"grant_type" => "password", "username" => name, "password" => password} ->
{name, password}
end
with {_, %User{} = user} <- {:user, User.get_by_nickname_or_email(name)},
{_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do
@ -20,9 +29,72 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
end
end
def get_registration(%Plug.Conn{
assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth}
}) do
registration = Registration.get_by_provider_uid(provider, uid)
if registration do
{:ok, registration}
else
info = auth.info
%Registration{}
|> Registration.changeset(%{
provider: to_string(provider),
uid: to_string(uid),
info: %{
"nickname" => info.nickname,
"email" => info.email,
"name" => info.name,
"description" => info.description
}
})
|> Repo.insert()
end
end
def get_registration(%Plug.Conn{} = _conn), do: {:error, :missing_credentials}
def create_from_registration(
%Plug.Conn{params: %{"authorization" => registration_attrs}},
registration
) do
nickname = value([registration_attrs["nickname"], Registration.nickname(registration)])
email = value([registration_attrs["email"], Registration.email(registration)])
name = value([registration_attrs["name"], Registration.name(registration)]) || nickname
bio = value([registration_attrs["bio"], Registration.description(registration)])
random_password = :crypto.strong_rand_bytes(64) |> Base.encode64()
with {:ok, new_user} <-
User.register_changeset(
%User{},
%{
email: email,
nickname: nickname,
name: name,
bio: bio,
password: random_password,
password_confirmation: random_password
},
external: true,
confirmed: true
)
|> Repo.insert(),
{:ok, _} <-
Registration.changeset(registration, %{user_id: new_user.id}) |> Repo.update() do
{:ok, new_user}
end
end
defp value(list), do: Enum.find(list, &(to_string(&1) != ""))
def handle_error(%Plug.Conn{} = _conn, error) do
error
end
def auth_template, do: nil
def oauth_consumer_template, do: nil
end

View file

@ -24,7 +24,7 @@ defmodule Pleroma.Web.UserSocket do
def connect(%{"token" => token}, socket) do
with true <- Pleroma.Config.get([:chat, :enabled]),
{:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84_600),
%User{} = user <- Pleroma.Repo.get(User, user_id) do
%User{} = user <- Pleroma.User.get_cached_by_id(user_id) do
{:ok, assign(socket, :user_name, user.nickname)}
else
_e -> :error

View file

@ -4,9 +4,9 @@
defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.ThreadMute
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
@ -64,8 +64,9 @@ defmodule Pleroma.Web.CommonAPI do
end
def delete(activity_id, user) do
with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id),
%Object{} = object <- Object.normalize(object_id),
with %Activity{data: %{"object" => _}} = activity <-
Activity.get_by_id_with_object(activity_id),
%Object{} = object <- Object.normalize(activity),
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
{:ok, _} <- unpin(activity_id, user),
{:ok, delete} <- ActivityPub.delete(object) do
@ -75,7 +76,7 @@ defmodule Pleroma.Web.CommonAPI do
def repeat(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity.data["object"]["id"]),
object <- Object.normalize(activity),
nil <- Utils.get_existing_announce(user.ap_id, object) do
ActivityPub.announce(user, object)
else
@ -86,7 +87,7 @@ defmodule Pleroma.Web.CommonAPI do
def unrepeat(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity.data["object"]["id"]) do
object <- Object.normalize(activity) do
ActivityPub.unannounce(user, object)
else
_ ->
@ -96,7 +97,7 @@ defmodule Pleroma.Web.CommonAPI do
def favorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity.data["object"]["id"]),
object <- Object.normalize(activity),
nil <- Utils.get_existing_like(user.ap_id, object) do
ActivityPub.like(user, object)
else
@ -107,7 +108,7 @@ defmodule Pleroma.Web.CommonAPI do
def unfavorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity.data["object"]["id"]) do
object <- Object.normalize(activity) do
ActivityPub.unlike(user, object)
else
_ ->
@ -125,7 +126,10 @@ defmodule Pleroma.Web.CommonAPI do
"public"
in_reply_to ->
Pleroma.Web.MastodonAPI.StatusView.get_visibility(in_reply_to.data["object"])
# XXX: these heuristics should be moved out of MastodonAPI.
with %Object{} = object <- Object.normalize(in_reply_to) do
Pleroma.Web.MastodonAPI.StatusView.get_visibility(object)
end
end
end
@ -142,7 +146,8 @@ defmodule Pleroma.Web.CommonAPI do
make_content_html(
status,
attachments,
data
data,
visibility
),
{to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility),
context <- make_context(in_reply_to),
@ -166,18 +171,21 @@ defmodule Pleroma.Web.CommonAPI do
object,
"emoji",
(Formatter.get_emoji(status) ++ Formatter.get_emoji(data["spoiler_text"]))
|> Enum.reduce(%{}, fn {name, file}, acc ->
|> Enum.reduce(%{}, fn {name, file, _}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
) do
res =
ActivityPub.create(%{
to: to,
actor: user,
context: context,
object: object,
additional: %{"cc" => cc, "directMessage" => visibility == "direct"}
})
ActivityPub.create(
%{
to: to,
actor: user,
context: context,
object: object,
additional: %{"cc" => cc, "directMessage" => visibility == "direct"}
},
Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
)
res
end
@ -210,8 +218,10 @@ defmodule Pleroma.Web.CommonAPI do
with %Activity{
actor: ^user_ap_id,
data: %{
"type" => "Create",
"object" => %{
"type" => "Create"
},
object: %Object{
data: %{
"to" => object_to,
"type" => "Note"
}
@ -273,9 +283,18 @@ defmodule Pleroma.Web.CommonAPI do
end
end
def bookmarked?(user, activity) do
with %Bookmark{} <- Bookmark.get(user.id, activity.id) do
true
else
_ ->
false
end
end
def report(user, data) do
with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
{:account, %User{} = account} <- {:account, User.get_by_id(account_id)},
{:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
{:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
{:ok, statuses} <- get_report_statuses(account, data),
{:ok, activity} <-
@ -284,14 +303,9 @@ defmodule Pleroma.Web.CommonAPI do
actor: user,
account: account,
statuses: statuses,
content: content_html
content: content_html,
forward: data["forward"] || false
}) do
Enum.each(User.all_superusers(), fn superuser ->
superuser
|> Pleroma.AdminEmail.report(user, account, statuses, content_html)
|> Pleroma.Mailer.deliver_async()
end)
{:ok, activity}
else
{:error, err} -> {:error, err}
@ -299,4 +313,24 @@ defmodule Pleroma.Web.CommonAPI do
{:account, nil} -> {:error, "Account not found"}
end
end
def hide_reblogs(user, muted) do
ap_id = muted.ap_id
if ap_id not in user.info.muted_reblogs do
info_changeset = User.Info.add_reblog_mute(user.info, ap_id)
changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
User.update_and_set_cache(changeset)
end
end
def show_reblogs(user, muted) do
ap_id = muted.ap_id
if ap_id in user.info.muted_reblogs do
info_changeset = User.Info.remove_reblog_mute(user.info, ap_id)
changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
User.update_and_set_cache(changeset)
end
end
end

View file

@ -12,25 +12,29 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MediaProxy
require Logger
# This is a hack for twidere.
def get_by_id_or_ap_id(id) do
activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id)
activity =
Activity.get_by_id_with_object(id) || Activity.get_create_by_object_ap_id_with_object(id)
activity &&
if activity.data["type"] == "Create" do
activity
else
Activity.get_create_by_object_ap_id(activity.data["object"])
Activity.get_create_by_object_ap_id_with_object(activity.data["object"])
end
end
def get_replied_to_activity(""), do: nil
def get_replied_to_activity(id) when not is_nil(id) do
Repo.get(Activity, id)
Activity.get_by_id(id)
end
def get_replied_to_activity(_), do: nil
@ -101,7 +105,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def make_content_html(
status,
attachments,
data
data,
visibility
) do
no_attachment_links =
data
@ -110,8 +115,15 @@ defmodule Pleroma.Web.CommonAPI.Utils do
content_type = get_content_type(data["content_type"])
options =
if visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
[safe_mention: true]
else
[]
end
status
|> format_input(content_type)
|> format_input(content_type, options)
|> maybe_add_attachments(attachments, no_attachment_links)
|> maybe_add_nsfw_tag(data)
end
@ -170,6 +182,18 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end).()
end
@doc """
Formatting text as BBCode.
"""
def format_input(text, "text/bbcode", options) do
text
|> String.replace(~r/\r/, "")
|> Formatter.html_escape("text/plain")
|> BBCode.to_html()
|> (fn {:ok, html} -> html end).()
|> Formatter.linkify(options)
end
@doc """
Formatting text to html.
"""
@ -183,11 +207,10 @@ defmodule Pleroma.Web.CommonAPI.Utils do
Formatting text to markdown.
"""
def format_input(text, "text/markdown", options) do
options = Keyword.put(options, :mentions_escape, true)
text
|> Formatter.mentions_escape(options)
|> Earmark.as_html!()
|> Formatter.linkify(options)
|> (fn {text, mentions, tags} -> {Earmark.as_html!(text), mentions, tags} end).()
|> Formatter.html_escape("text/html")
end
@ -197,7 +220,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
context,
content_html,
attachments,
inReplyTo,
in_reply_to,
tags,
cw \\ nil,
cc \\ []
@ -214,10 +237,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
}
if inReplyTo do
if in_reply_to do
in_reply_to_object = Object.normalize(in_reply_to)
object
|> Map.put("inReplyTo", inReplyTo.data["object"]["id"])
|> Map.put("inReplyToStatusId", inReplyTo.id)
|> Map.put("inReplyTo", in_reply_to_object.data["id"])
else
object
end
@ -231,15 +255,21 @@ defmodule Pleroma.Web.CommonAPI.Utils do
Strftime.strftime!(date, "%a %b %d %H:%M:%S %z %Y")
end
def date_to_asctime(date) do
with {:ok, date, _offset} <- date |> DateTime.from_iso8601() do
def date_to_asctime(date) when is_binary(date) do
with {:ok, date, _offset} <- DateTime.from_iso8601(date) do
format_asctime(date)
else
_e ->
Logger.warn("Date #{date} in wrong format, must be ISO 8601")
""
end
end
def date_to_asctime(date) do
Logger.warn("Date #{date} in wrong format, must be ISO 8601")
""
end
def to_masto_date(%NaiveDateTime{} = date) do
date
|> NaiveDateTime.to_iso8601()
@ -266,7 +296,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
def confirm_current_password(user, password) do
with %User{local: true} = db_user <- Repo.get(User, user.id),
with %User{local: true} = db_user <- User.get_cached_by_id(user.id),
true <- Pbkdf2.checkpw(password, db_user.password_hash) do
{:ok, db_user}
else
@ -276,7 +306,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def emoji_from_profile(%{info: _info} = user) do
(Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name))
|> Enum.map(fn {shortcode, url} ->
|> Enum.map(fn {shortcode, url, _} ->
%{
"type" => "Emoji",
"icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"},
@ -294,10 +324,10 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def maybe_notify_mentioned_recipients(
recipients,
%Activity{data: %{"to" => _to, "type" => type} = data} = _activity
%Activity{data: %{"to" => _to, "type" => type} = data} = activity
)
when type == "Create" do
object = Object.normalize(data["object"])
object = Object.normalize(activity)
object_data =
cond do
@ -318,6 +348,24 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def maybe_notify_mentioned_recipients(recipients, _), do: recipients
def maybe_notify_subscribers(
recipients,
%Activity{data: %{"actor" => actor, "type" => type}} = activity
)
when type == "Create" do
with %User{} = user <- User.get_cached_by_ap_id(actor) do
subscriber_ids =
user
|> User.subscribers()
|> Enum.filter(&Visibility.visible_for_user?(activity, &1))
|> Enum.map(& &1.ap_id)
recipients ++ subscriber_ids
end
end
def maybe_notify_subscribers(recipients, _), do: recipients
def maybe_extract_mentions(%{"tag" => tag}) do
tag
|> Enum.filter(fn x -> is_map(x) end)
@ -344,4 +392,33 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
def get_report_statuses(_, _), do: {:ok, nil}
# DEPRECATED mostly, context objects are now created at insertion time.
def context_to_conversation_id(context) do
with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
id
else
_e ->
changeset = Object.context_mapping(context)
case Repo.insert(changeset) do
{:ok, %{id: id}} ->
id
# This should be solved by an upsert, but it seems ecto
# has problems accessing the constraint inside the jsonb.
{:error, _} ->
Object.get_cached_by_ap_id(context).id
end
end
end
def conversation_id_to_context(id) do
with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
context
else
_e ->
{:error, "No such conversation"}
end
end
end

View file

@ -5,6 +5,11 @@
defmodule Pleroma.Web.ControllerHelper do
use Pleroma.Web, :controller
# As in MastoAPI, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
@falsy_param_values [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"]
def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil
def truthy_param?(value), do: value not in @falsy_param_values
def oauth_scopes(params, default) do
# Note: `scopes` is used by Mastodon — supporting it but sticking to
# OAuth's standard `scope` wherever we control it

View file

@ -25,7 +25,7 @@ defmodule Pleroma.Web.Endpoint do
at: "/",
from: :pleroma,
only:
~w(index.html static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc)
~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc)
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
)
@ -51,11 +51,17 @@ defmodule Pleroma.Web.Endpoint do
plug(Plug.MethodOverride)
plug(Plug.Head)
secure_cookies = Pleroma.Config.get([__MODULE__, :secure_cookie_flag])
cookie_name =
if Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag),
if secure_cookies,
do: "__Host-pleroma_key",
else: "pleroma_key"
extra =
Pleroma.Config.get([__MODULE__, :extra_cookie_attrs])
|> Enum.join(";")
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
@ -65,11 +71,30 @@ defmodule Pleroma.Web.Endpoint do
key: cookie_name,
signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]},
http_only: true,
secure:
Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag),
extra: "SameSite=Strict"
secure: secure_cookies,
extra: extra
)
# Note: the plug and its configuration is compile-time this can't be upstreamed yet
if proxies = Pleroma.Config.get([__MODULE__, :reverse_proxies]) do
plug(RemoteIp, proxies: proxies)
end
defmodule Instrumenter do
use Prometheus.PhoenixInstrumenter
end
defmodule PipelineInstrumenter do
use Prometheus.PlugPipelineInstrumenter
end
defmodule MetricsExporter do
use Prometheus.PlugExporter
end
plug(PipelineInstrumenter)
plug(MetricsExporter)
plug(Pleroma.Web.Router)
@doc """

View file

@ -4,7 +4,7 @@
defmodule Pleroma.Web.Federator do
alias Pleroma.Activity
alias Pleroma.Jobs
alias Pleroma.Object.Containment
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
@ -31,39 +31,39 @@ defmodule Pleroma.Web.Federator do
# Client API
def incoming_doc(doc) do
Jobs.enqueue(:federator_incoming, __MODULE__, [:incoming_doc, doc])
PleromaJobQueue.enqueue(:federator_incoming, __MODULE__, [:incoming_doc, doc])
end
def incoming_ap_doc(params) do
Jobs.enqueue(:federator_incoming, __MODULE__, [:incoming_ap_doc, params])
PleromaJobQueue.enqueue(:federator_incoming, __MODULE__, [:incoming_ap_doc, params])
end
def publish(activity, priority \\ 1) do
Jobs.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority)
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority)
end
def publish_single_ap(params) do
Jobs.enqueue(:federator_outgoing, __MODULE__, [:publish_single_ap, params])
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_ap, params])
end
def publish_single_websub(websub) do
Jobs.enqueue(:federator_outgoing, __MODULE__, [:publish_single_websub, websub])
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_websub, websub])
end
def verify_websub(websub) do
Jobs.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub])
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub])
end
def request_subscription(sub) do
Jobs.enqueue(:federator_outgoing, __MODULE__, [:request_subscription, sub])
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:request_subscription, sub])
end
def refresh_subscriptions do
Jobs.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions])
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions])
end
def publish_single_salmon(params) do
Jobs.enqueue(:federator_outgoing, __MODULE__, [:publish_single_salmon, params])
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_salmon, params])
end
# Job Worker Callbacks
@ -137,7 +137,7 @@ defmodule Pleroma.Web.Federator do
# actor shouldn't be acting on objects outside their own AP server.
with {:ok, _user} <- ap_enabled_actor(params["actor"]),
nil <- Activity.normalize(params["id"]),
:ok <- Transmogrifier.contain_origin_from_id(params["actor"], params),
:ok <- Containment.contain_origin_from_id(params["actor"], params),
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, activity}
else
@ -186,7 +186,7 @@ defmodule Pleroma.Web.Federator do
end
def ap_enabled_actor(id) do
user = User.get_by_ap_id(id)
user = User.get_cached_by_ap_id(id)
if User.ap_enabled?(user) do
{:ok, user}

View file

@ -2,61 +2,82 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
import Ecto.Query
import Ecto.Changeset
alias Pleroma.Repo
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Pagination
alias Pleroma.ScheduledActivity
alias Pleroma.User
alias Pleroma.Web.CommonAPI
@default_limit 20
def follow(follower, followed, params \\ %{}) do
options = cast_params(params)
reblogs = options[:reblogs]
result =
if not User.following?(follower, followed) do
CommonAPI.follow(follower, followed)
else
{:ok, follower, followed, nil}
end
with {:ok, follower, followed, _} <- result do
reblogs
|> case do
false -> CommonAPI.hide_reblogs(follower, followed)
_ -> CommonAPI.show_reblogs(follower, followed)
end
|> case do
{:ok, follower} -> {:ok, follower}
_ -> {:ok, follower}
end
end
end
def get_followers(user, params \\ %{}) do
user
|> User.get_followers_query()
|> paginate(params)
|> Repo.all()
|> Pagination.fetch_paginated(params)
end
def get_friends(user, params \\ %{}) do
user
|> User.get_friends_query()
|> paginate(params)
|> Repo.all()
|> Pagination.fetch_paginated(params)
end
def paginate(query, params \\ %{}) do
def get_notifications(user, params \\ %{}) do
options = cast_params(params)
query
|> restrict(:max_id, options)
|> restrict(:since_id, options)
|> restrict(:limit, options)
|> order_by([u], fragment("? desc nulls last", u.id))
user
|> Notification.for_user_query()
|> restrict(:exclude_types, options)
|> Pagination.fetch_paginated(params)
end
def cast_params(params) do
def get_scheduled_activities(user, params \\ %{}) do
user
|> ScheduledActivity.for_user_query()
|> Pagination.fetch_paginated(params)
end
defp cast_params(params) do
param_types = %{
max_id: :string,
since_id: :string,
limit: :integer
exclude_types: {:array, :string},
reblogs: :boolean
}
changeset = cast({%{}, param_types}, params, Map.keys(param_types))
changeset.changes
end
defp restrict(query, :max_id, %{max_id: max_id}) do
query
|> where([q], q.id < ^max_id)
end
defp restrict(query, :since_id, %{since_id: since_id}) do
query
|> where([q], q.id > ^since_id)
end
defp restrict(query, :limit, options) do
limit = Map.get(options, :limit, @default_limit)
defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
ap_types =
mastodon_types
|> Enum.map(&Activity.from_mastodon_notification_type/1)
|> Enum.filter(& &1)
query
|> limit(^limit)
|> where([q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
end
defp restrict(query, _, _), do: query

View file

@ -4,13 +4,17 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
alias Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Config
alias Pleroma.Filter
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web
@ -18,18 +22,21 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.AppView
alias Pleroma.Web.MastodonAPI.FilterView
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonView
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.ReportView
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
alias Pleroma.Web.ControllerHelper
import Ecto.Query
require Logger
@ -40,7 +47,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
action_fallback(:errors)
def create_app(conn, params) do
scopes = oauth_scopes(params, ["read"])
scopes = ControllerHelper.oauth_scopes(params, ["read"])
app_attrs =
params
@ -50,16 +57,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
with cs <- App.register_changeset(%App{}, app_attrs),
false <- cs.changes[:client_name] == @local_mastodon_name,
{:ok, app} <- Repo.insert(cs) do
res = %{
id: app.id |> to_string,
name: app.client_name,
client_id: app.client_id,
client_secret: app.client_secret,
redirect_uri: app.redirect_uris,
website: app.website
}
json(conn, res)
conn
|> put_view(AppView)
|> render("show.json", %{app: app})
end
end
@ -97,8 +97,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end)
info_params =
%{}
|> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
[:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
|> Enum.reduce(%{}, fn key, acc ->
add_if_present(acc, params, to_string(key), key, fn value ->
{:ok, ControllerHelper.truthy_param?(value)}
end)
end)
|> add_if_present(params, "default_scope", :default_scope)
|> add_if_present(params, "header", :banner, fn value ->
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :banner) do
@ -108,7 +113,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end)
info_cng = User.Info.mastodon_profile_update(user.info, info_params)
info_cng = User.Info.profile_update(user.info, info_params)
with changeset <- User.update_changeset(user, user_params),
changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
@ -131,6 +136,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
json(conn, account)
end
def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
conn
|> put_view(AppView)
|> render("short.json", %{app: app})
end
end
def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
@ -160,6 +173,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
},
stats: Stats.get_stats(),
thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
languages: ["en"],
registrations: Pleroma.Config.get([:instance, :registrations_open]),
# Extra (not present in Mastodon):
max_toot_chars: Keyword.get(instance, :limit)
}
@ -172,14 +188,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
defp mastodonized_emoji do
Pleroma.Emoji.get_all()
|> Enum.map(fn {shortcode, relative_url} ->
|> Enum.map(fn {shortcode, relative_url, tags} ->
url = to_string(URI.merge(Web.base_url(), relative_url))
%{
"shortcode" => shortcode,
"static_url" => url,
"visible_in_picker" => true,
"url" => url
"url" => url,
"tags" => tags
}
end)
end
@ -192,15 +209,29 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
params =
conn.params
|> Map.drop(["since_id", "max_id"])
|> Map.drop(["since_id", "max_id", "min_id"])
|> Map.merge(params)
last = List.last(activities)
first = List.first(activities)
if last do
min = last.id
max = first.id
max_id = last.id
limit =
params
|> Map.get("limit", "20")
|> String.to_integer()
min_id =
if length(activities) <= limit do
activities
|> List.first()
|> Map.get(:id)
else
activities
|> Enum.at(limit * -1)
|> Map.get(:id)
end
{next_url, prev_url} =
if param do
@ -209,13 +240,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{max_id: min})
Map.merge(params, %{max_id: max_id})
),
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{since_id: max})
Map.merge(params, %{min_id: min_id})
)
}
else
@ -223,12 +254,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{max_id: min})
Map.merge(params, %{max_id: max_id})
),
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{since_id: max})
Map.merge(params, %{min_id: min_id})
)
}
end
@ -254,6 +285,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> ActivityPub.contain_timeline(user)
|> Enum.reverse()
user = Repo.preload(user, bookmarks: :activity)
conn
|> add_link_headers(:home_timeline, activities)
|> put_view(StatusView)
@ -272,6 +305,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()
user = Repo.preload(user, bookmarks: :activity)
conn
|> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
|> put_view(StatusView)
@ -279,7 +314,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- Repo.get(User, params["id"]) do
with %User{} = user <- User.get_cached_by_id(params["id"]),
reading_user <- Repo.preload(reading_user, :bookmarks) do
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
conn
@ -304,7 +340,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
activities =
[user.ap_id]
|> ActivityPub.fetch_activities_query(params)
|> Repo.all()
|> Pagination.fetch_paginated(params)
user = Repo.preload(user, bookmarks: :activity)
conn
|> add_link_headers(:dm_timeline, activities)
@ -313,8 +351,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
user = Repo.preload(user, bookmarks: :activity)
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user})
@ -322,7 +362,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
with %Activity{} = activity <- Activity.get_by_id(id),
activities <-
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
"blocking_user" => user,
@ -358,6 +398,55 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
conn
|> add_link_headers(:scheduled_statuses, scheduled_activities)
|> put_view(ScheduledActivityView)
|> render("index.json", %{scheduled_activities: scheduled_activities})
end
end
def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
with %ScheduledActivity{} = scheduled_activity <-
ScheduledActivity.get(user, scheduled_activity_id) do
conn
|> put_view(ScheduledActivityView)
|> render("show.json", %{scheduled_activity: scheduled_activity})
else
_ -> {:error, :not_found}
end
end
def update_scheduled_status(
%{assigns: %{user: user}} = conn,
%{"id" => scheduled_activity_id} = params
) do
with %ScheduledActivity{} = scheduled_activity <-
ScheduledActivity.get(user, scheduled_activity_id),
{:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
conn
|> put_view(ScheduledActivityView)
|> render("show.json", %{scheduled_activity: scheduled_activity})
else
nil -> {:error, :not_found}
error -> error
end
end
def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
with %ScheduledActivity{} = scheduled_activity <-
ScheduledActivity.get(user, scheduled_activity_id),
{:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
conn
|> put_view(ScheduledActivityView)
|> render("show.json", %{scheduled_activity: scheduled_activity})
else
nil -> {:error, :not_found}
error -> error
end
end
def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
when length(media_ids) > 0 do
params =
@ -378,12 +467,27 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
_ -> Ecto.UUID.generate()
end
{:ok, activity} =
Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
scheduled_at = params["scheduled_at"]
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
with {:ok, scheduled_activity} <-
ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
conn
|> put_view(ScheduledActivityView)
|> render("show.json", %{scheduled_activity: scheduled_activity})
end
else
params = Map.drop(params, ["scheduled_at"])
{:ok, activity} =
Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
CommonAPI.post(user, params)
end)
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
end
def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
@ -398,7 +502,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
%Activity{} = announce <- Activity.normalize(announce.data) do
user = Repo.preload(user, bookmarks: :activity)
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: announce, for: user, as: :activity})
@ -407,7 +514,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
%Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
user = Repo.preload(user, bookmarks: :activity)
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
@ -454,10 +563,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
%User{} = user <- User.get_by_nickname(user.nickname),
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
{:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
{:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
user = Repo.preload(user, bookmarks: :activity)
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
@ -465,10 +576,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
%User{} = user <- User.get_by_nickname(user.nickname),
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
{:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
{:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
user = Repo.preload(user, bookmarks: :activity)
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
@ -501,21 +614,19 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params)
result =
notifications
|> Enum.map(fn x -> render_notification(user, x) end)
|> Enum.filter(& &1)
notifications = MastodonAPI.get_notifications(user, params)
conn
|> add_link_headers(:notifications, notifications)
|> json(result)
|> put_view(NotificationView)
|> render("index.json", %{notifications: notifications, for: user})
end
def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, notification} <- Notification.get(user, id) do
json(conn, render_notification(user, notification))
conn
|> put_view(NotificationView)
|> render("show.json", %{notification: notification, for: user})
else
{:error, reason} ->
conn
@ -540,6 +651,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
Notification.destroy_multiple(user, ids)
json(conn, %{})
end
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
id = List.wrap(id)
q = from(u in User, where: u.id in ^id)
@ -589,7 +705,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def favourited_by(conn, %{"id" => id}) do
with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
%Object{data: %{"likes" => likes}} <- Object.normalize(object) do
q = from(u in User, where: u.ap_id in ^likes)
users = Repo.all(q)
@ -602,7 +719,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def reblogged_by(conn, %{"id" => id}) do
with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
%Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
q = from(u in User, where: u.ap_id in ^announces)
users = Repo.all(q)
@ -653,7 +771,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
with %User{} = user <- Repo.get(User, id),
with %User{} = user <- User.get_cached_by_id(id),
followers <- MastodonAPI.get_followers(user, params) do
followers =
cond do
@ -670,7 +788,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
with %User{} = user <- Repo.get(User, id),
with %User{} = user <- User.get_cached_by_id(id),
followers <- MastodonAPI.get_friends(user, params) do
followers =
cond do
@ -695,7 +813,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
with %User{} = follower <- Repo.get(User, id),
with %User{} = follower <- User.get_cached_by_id(id),
{:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
conn
|> put_view(AccountView)
@ -709,7 +827,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
with %User{} = follower <- Repo.get(User, id),
with %User{} = follower <- User.get_cached_by_id(id),
{:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
conn
|> put_view(AccountView)
@ -723,12 +841,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id),
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
{_, true} <- {:followed, follower.id != followed.id},
{:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: follower, target: followed})
else
{:followed, _} ->
{:error, :not_found}
{:error, message} ->
conn
|> put_resp_content_type("application/json")
@ -737,12 +859,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with %User{} = followed <- Repo.get_by(User, nickname: uri),
with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
{_, true} <- {:followed, follower.id != followed.id},
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
conn
|> put_view(AccountView)
|> render("account.json", %{user: followed, for: follower})
else
{:followed, _} ->
{:error, :not_found}
{:error, message} ->
conn
|> put_resp_content_type("application/json")
@ -751,16 +877,23 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id),
with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
{_, true} <- {:followed, follower.id != followed.id},
{:ok, follower} <- CommonAPI.unfollow(follower, followed) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: follower, target: followed})
else
{:followed, _} ->
{:error, :not_found}
error ->
error
end
end
def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
with %User{} = muted <- Repo.get(User, id),
with %User{} = muted <- User.get_cached_by_id(id),
{:ok, muter} <- User.mute(muter, muted) do
conn
|> put_view(AccountView)
@ -774,7 +907,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
with %User{} = muted <- Repo.get(User, id),
with %User{} = muted <- User.get_cached_by_id(id),
{:ok, muter} <- User.unmute(muter, muted) do
conn
|> put_view(AccountView)
@ -795,7 +928,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- Repo.get(User, id),
with %User{} = blocked <- User.get_cached_by_id(id),
{:ok, blocker} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
conn
@ -810,7 +943,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- Repo.get(User, id),
with %User{} = blocked <- User.get_cached_by_id(id),
{:ok, blocker} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
conn
@ -845,10 +978,38 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
json(conn, %{})
end
def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %User{} = subscription_target <- User.get_cached_by_id(id),
{:ok, subscription_target} = User.subscribe(user, subscription_target) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: user, target: subscription_target})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %User{} = subscription_target <- User.get_cached_by_id(id),
{:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: user, target: subscription_target})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def status_search(user, query) do
fetched =
if Regex.match?(~r/https?:/, query) do
with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
with {:ok, object} <- Fetcher.fetch_object_from_id(query),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user) do
[activity]
@ -859,13 +1020,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
q =
from(
a in Activity,
[a, o] in Activity.with_preloaded_object(Activity),
where: fragment("?->>'type' = 'Create'", a.data),
where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
where:
fragment(
"to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
a.data,
"to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
o.data,
^query
),
limit: 20,
@ -931,29 +1092,75 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def favourites(%{assigns: %{user: user}} = conn, params) do
activities =
params =
params
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user)
|> ActivityPub.fetch_public_activities()
activities =
ActivityPub.fetch_activities([], params)
|> Enum.reverse()
user = Repo.preload(user, bookmarks: :activity)
conn
|> add_link_headers(:favourites, activities)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
def bookmarks(%{assigns: %{user: user}} = conn, _) do
user = Repo.get(User, user.id)
def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
with %User{} = user <- User.get_by_id(id),
false <- user.info.hide_favorites do
params =
params
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", for_user)
recipients =
if for_user do
["https://www.w3.org/ns/activitystreams#Public"] ++
[for_user.ap_id | for_user.following]
else
["https://www.w3.org/ns/activitystreams#Public"]
end
activities =
recipients
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()
conn
|> add_link_headers(:favourites, activities)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: for_user, as: :activity})
else
nil ->
{:error, :not_found}
true ->
conn
|> put_status(403)
|> json(%{error: "Can't get favorites"})
end
end
def bookmarks(%{assigns: %{user: user}} = conn, params) do
user = User.get_cached_by_id(user.id)
user = Repo.preload(user, bookmarks: :activity)
bookmarks =
Bookmark.for_user_query(user.id)
|> Pagination.fetch_paginated(params)
activities =
user.bookmarks
|> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
|> Enum.reverse()
bookmarks
|> Enum.map(fn b -> b.activity end)
conn
|> add_link_headers(:bookmarks, bookmarks)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
@ -1003,7 +1210,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
accounts
|> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
%User{} = followed <- Repo.get(User, account_id) do
%User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.follow(list, followed)
end
end)
@ -1015,7 +1222,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
accounts
|> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
%User{} = followed <- Repo.get(Pleroma.User, account_id) do
%User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
Pleroma.List.unfollow(list, followed)
end
end)
@ -1059,6 +1266,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse()
user = Repo.preload(user, bookmarks: :activity)
conn
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
@ -1071,9 +1280,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def index(%{assigns: %{user: user}} = conn, _params) do
token =
conn
|> get_session(:oauth_token)
token = get_session(conn, :oauth_token)
if user && token do
mastodon_emoji = mastodonized_emoji()
@ -1101,7 +1308,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
auto_play_gif: false,
display_sensitive_media: false,
reduce_motion: false,
max_toot_chars: limit
max_toot_chars: limit,
mascot: "/images/pleroma-fox-tan-smol.png"
},
rights: %{
delete_others_notice: present?(user.info.is_moderator),
@ -1173,6 +1381,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> render("index.html", %{initial_state: initial_state, flavour: flavour})
else
conn
|> put_session(:return_to, conn.request_path)
|> redirect(to: "/web/login")
end
end
@ -1229,16 +1438,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
"glitch"
end
def login(conn, %{"code" => code}) do
def login(%{assigns: %{user: %User{}}} = conn, _params) do
redirect(conn, to: local_mastodon_root_path(conn))
end
@doc "Local Mastodon FE login init action"
def login(conn, %{"code" => auth_token}) do
with {:ok, app} <- get_or_make_app(),
%Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
%Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
{:ok, token} <- Token.exchange_token(app, auth) do
conn
|> put_session(:oauth_token, token.token)
|> redirect(to: "/web/getting-started")
|> redirect(to: local_mastodon_root_path(conn))
end
end
@doc "Local Mastodon FE callback action"
def login(conn, _) do
with {:ok, app} <- get_or_make_app() do
path =
@ -1251,8 +1466,18 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
scope: Enum.join(app.scopes, " ")
)
conn
|> redirect(to: path)
redirect(conn, to: path)
end
end
defp local_mastodon_root_path(conn) do
case get_session(conn, :return_to) do
nil ->
mastodon_api_path(conn, :index, ["getting-started"])
return_to ->
delete_session(conn, :return_to)
return_to
end
end
@ -1292,7 +1517,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
Logger.debug("Unimplemented, returning unmodified relationship")
with %User{} = target <- Repo.get(User, id) do
with %User{} = target <- User.get_cached_by_id(id) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: user, target: target})
@ -1309,45 +1534,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
json(conn, %{})
end
def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
actor = User.get_cached_by_ap_id(activity.data["actor"])
parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
mastodon_type = Activity.mastodon_notification_type(activity)
response = %{
id: to_string(id),
type: mastodon_type,
created_at: CommonAPI.Utils.to_masto_date(created_at),
account: AccountView.render("account.json", %{user: actor, for: user})
}
case mastodon_type do
"mention" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: activity, for: user})
})
"favourite" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
})
"reblog" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
})
"follow" ->
response
_ ->
nil
end
end
def get_filters(%{assigns: %{user: user}} = conn, _) do
filters = Filter.get_filters(user)
res = FilterView.render("filters.json", filters: filters)
@ -1409,6 +1595,23 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
# fallback action
#
def errors(conn, {:error, %Changeset{} = changeset}) do
error_message =
changeset
|> Changeset.traverse_errors(fn {message, _opt} -> message end)
|> Enum.map_join(", ", fn {_k, v} -> v end)
conn
|> put_status(422)
|> json(%{error: error_message})
end
def errors(conn, {:error, :not_found}) do
conn
|> put_status(404)
|> json(%{error: "Record not found"})
end
def errors(conn, _) do
conn
|> put_status(500)
@ -1473,7 +1676,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
with %Activity{} = activity <- Repo.get(Activity, status_id),
with %Activity{} = activity <- Activity.get_by_id(status_id),
true <- Visibility.visible_for_user?(activity, user) do
data =
StatusView.render(

View file

@ -53,9 +53,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
blocking: User.blocks?(user, target),
muting: User.mutes?(user, target),
muting_notifications: false,
subscribing: User.subscribed_to?(user, target),
requested: requested,
domain_blocking: false,
showing_reblogs: false,
showing_reblogs: User.showing_reblogs?(user, target),
endorsed: false
}
end
@ -67,7 +68,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp do_render("account.json", %{user: user} = opts) do
image = User.avatar_url(user) |> MediaProxy.url()
header = User.banner_url(user) |> MediaProxy.url()
user_info = User.user_info(user)
user_info = User.get_cached_user_info(user)
bot = (user.info.source_data["type"] || "Person") in ["Application", "Service"]
emojis =
@ -112,19 +113,23 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
bot: bot,
source: %{
note: "",
privacy: user_info.default_scope,
sensitive: false
sensitive: false,
pleroma: %{}
},
# Pleroma extension
pleroma: %{
confirmation_pending: user_info.confirmation_pending,
tags: user.tags,
is_moderator: user.info.is_moderator,
is_admin: user.info.is_admin,
hide_followers: user.info.hide_followers,
hide_follows: user.info.hide_follows,
hide_favorites: user.info.hide_favorites,
relationship: relationship
}
}
|> maybe_put_role(user, opts[:for])
|> maybe_put_settings(user, opts[:for], user_info)
|> maybe_put_notification_settings(user, opts[:for])
end
defp username_from_nickname(string) when is_binary(string) do
@ -132,4 +137,38 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
end
defp username_from_nickname(_), do: nil
defp maybe_put_settings(
data,
%User{id: user_id} = user,
%User{id: user_id},
user_info
) do
data
|> Kernel.put_in([:source, :privacy], user_info.default_scope)
|> Kernel.put_in([:source, :pleroma, :show_role], user.info.show_role)
|> Kernel.put_in([:source, :pleroma, :no_rich_text], user.info.no_rich_text)
end
defp maybe_put_settings(data, _, _, _), do: data
defp maybe_put_role(data, %User{info: %{show_role: true}} = user, _) do
data
|> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin)
|> Kernel.put_in([:pleroma, :is_moderator], user.info.is_moderator)
end
defp maybe_put_role(data, %User{id: user_id} = user, %User{id: user_id}) do
data
|> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin)
|> Kernel.put_in([:pleroma, :is_moderator], user.info.is_moderator)
end
defp maybe_put_role(data, _, _), do: data
defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do
Kernel.put_in(data, [:pleroma, :notification_settings], user.info.notification_settings)
end
defp maybe_put_notification_settings(data, _, _), do: data
end

View file

@ -0,0 +1,41 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AppView do
use Pleroma.Web, :view
alias Pleroma.Web.OAuth.App
@vapid_key :web_push_encryption
|> Application.get_env(:vapid_details, [])
|> Keyword.get(:public_key)
def render("show.json", %{app: %App{} = app}) do
%{
id: app.id |> to_string,
name: app.client_name,
client_id: app.client_id,
client_secret: app.client_secret,
redirect_uri: app.redirect_uris,
website: app.website
}
|> with_vapid_key()
end
def render("short.json", %{app: %App{website: webiste, client_name: name}}) do
%{
name: name,
website: webiste
}
|> with_vapid_key()
end
defp with_vapid_key(data) do
if @vapid_key do
Map.put(data, "vapid_key", @vapid_key)
else
data
end
end
end

View file

@ -0,0 +1,64 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.NotificationView do
use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{notifications: notifications, for: user}) do
render_many(notifications, NotificationView, "show.json", %{for: user})
end
def render("show.json", %{
notification: %Notification{activity: activity} = notification,
for: user
}) do
actor = User.get_cached_by_ap_id(activity.data["actor"])
parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
mastodon_type = Activity.mastodon_notification_type(activity)
response = %{
id: to_string(notification.id),
type: mastodon_type,
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
account: AccountView.render("account.json", %{user: actor, for: user}),
pleroma: %{
is_seen: notification.seen
}
}
case mastodon_type do
"mention" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: activity, for: user})
})
"favourite" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
})
"reblog" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
})
"follow" ->
response
_ ->
nil
end
end
end

View file

@ -0,0 +1,57 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
use Pleroma.Web, :view
alias Pleroma.ScheduledActivity
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{scheduled_activities: scheduled_activities}) do
render_many(scheduled_activities, ScheduledActivityView, "show.json")
end
def render("show.json", %{scheduled_activity: %ScheduledActivity{} = scheduled_activity}) do
%{
id: to_string(scheduled_activity.id),
scheduled_at: CommonAPI.Utils.to_masto_date(scheduled_activity.scheduled_at),
params: status_params(scheduled_activity.params)
}
|> with_media_attachments(scheduled_activity)
end
defp with_media_attachments(data, %{params: %{"media_attachments" => media_attachments}}) do
try do
attachments = render_many(media_attachments, StatusView, "attachment.json", as: :attachment)
Map.put(data, :media_attachments, attachments)
rescue
_ -> data
end
end
defp with_media_attachments(data, _), do: data
defp status_params(params) do
data = %{
text: params["status"],
sensitive: params["sensitive"],
spoiler_text: params["spoiler_text"],
visibility: params["visibility"],
scheduled_at: params["scheduled_at"],
poll: params["poll"],
in_reply_to_id: params["in_reply_to_id"]
}
data =
if media_ids = params["media_ids"] do
Map.put(data, :media_ids, media_ids)
else
data
end
data
end
end

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
@ -19,8 +20,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
defp get_replied_to_activities(activities) do
activities
|> Enum.map(fn
%{data: %{"type" => "Create", "object" => %{"inReplyTo" => in_reply_to}}} ->
in_reply_to != "" && in_reply_to
%{data: %{"type" => "Create", "object" => object}} ->
object = Object.normalize(object)
object.data["inReplyTo"] != "" && object.data["inReplyTo"]
_ ->
nil
@ -29,7 +31,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|> Activity.create_by_object_ap_id()
|> Repo.all()
|> Enum.reduce(%{}, fn activity, acc ->
Map.put(acc, activity.data["object"]["id"], activity)
object = Object.normalize(activity)
Map.put(acc, object.data["id"], activity)
end)
end
@ -46,6 +49,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
end
defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
do: context_id
defp get_context_id(%{data: %{"context" => context}}) when is_binary(context),
do: Utils.context_to_conversation_id(context)
defp get_context_id(_), do: nil
defp reblogged?(activity, user) do
object = Object.normalize(activity) || %{}
present?(user && user.ap_id in (object.data["announcements"] || []))
end
def render("index.json", opts) do
replied_to_activities = get_replied_to_activities(opts.activities)
@ -64,8 +80,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
user = get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])
reblogged = Activity.get_create_by_object_ap_id(object)
reblogged = render("status.json", Map.put(opts, :activity, reblogged))
reblogged_activity = Activity.get_create_by_object_ap_id(object)
reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity))
activity_object = Object.normalize(activity)
favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
bookmarked = opts[:for] && CommonAPI.bookmarked?(opts[:for], reblogged_activity)
mentions =
activity.recipients
@ -86,9 +107,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
reblogs_count: 0,
replies_count: 0,
favourites_count: 0,
reblogged: false,
favourited: false,
bookmarked: false,
reblogged: reblogged?(reblogged_activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmarked),
muted: false,
pinned: pinned?(activity, user),
sensitive: false,
@ -109,14 +130,16 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end
def render("status.json", %{activity: %{data: %{"object" => object}} = activity} = opts) do
def render("status.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
object = Object.normalize(activity)
user = get_user(activity.data["actor"])
like_count = object["like_count"] || 0
announcement_count = object["announcement_count"] || 0
like_count = object.data["like_count"] || 0
announcement_count = object.data["announcement_count"] || 0
tags = object["tag"] || []
sensitive = object["sensitive"] || Enum.member?(tags, "nsfw")
tags = object.data["tag"] || []
sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
mentions =
activity.recipients
@ -124,25 +147,53 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|> Enum.filter(& &1)
|> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || [])
favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
bookmarked = opts[:for] && object["id"] in opts[:for].bookmarks
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
attachment_data = object["attachment"] || []
bookmarked = opts[:for] && CommonAPI.bookmarked?(opts[:for], activity)
attachment_data = object.data["attachment"] || []
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
created_at = Utils.to_masto_date(object["published"])
created_at = Utils.to_masto_date(object.data["published"])
reply_to = get_reply_to(activity, opts)
reply_to_user = reply_to && get_user(reply_to.data["actor"])
content =
object
|> render_content()
|> HTML.get_cached_scrubbed_html_for_object(
content_html =
content
|> HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
__MODULE__
"mastoapi:content"
)
content_plaintext =
content
|> HTML.get_cached_stripped_html_for_activity(
activity,
"mastoapi:content"
)
summary = object.data["summary"] || ""
summary_html =
summary
|> HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
"mastoapi:summary"
)
summary_plaintext =
summary
|> HTML.get_cached_stripped_html_for_activity(
activity,
"mastoapi:summary"
)
card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
@ -151,30 +202,30 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
if user.local do
Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
else
object["external_url"] || object["id"]
object.data["external_url"] || object.data["id"]
end
%{
id: to_string(activity.id),
uri: object["id"],
uri: object.data["id"],
url: url,
account: AccountView.render("account.json", %{user: user}),
in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil,
card: card,
content: content,
content: content_html,
created_at: created_at,
reblogs_count: announcement_count,
replies_count: 0,
replies_count: object.data["repliesCount"] || 0,
favourites_count: like_count,
reblogged: present?(repeated),
reblogged: reblogged?(activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmarked),
muted: CommonAPI.thread_muted?(user, activity) || User.mutes?(opts[:for], user),
pinned: pinned?(activity, user),
sensitive: sensitive,
spoiler_text: object["summary"] || "",
spoiler_text: summary_html,
visibility: get_visibility(object),
media_attachments: attachments,
mentions: mentions,
@ -184,9 +235,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
website: nil
},
language: nil,
emojis: build_emojis(activity.data["object"]["emoji"]),
emojis: build_emojis(object.data["emoji"]),
pleroma: %{
local: activity.local
local: activity.local,
conversation_id: get_context_id(activity),
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary_plaintext}
}
}
end
@ -257,18 +312,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
preview_url: href,
text_url: href,
type: type,
description: attachment["name"]
description: attachment["name"],
pleroma: %{mime_type: media_type}
}
end
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
_id = activity.data["object"]["inReplyTo"]
replied_to_activities[activity.data["object"]["inReplyTo"]]
object = Object.normalize(activity)
with nil <- replied_to_activities[object.data["inReplyTo"]] do
# If user didn't participate in the thread
Activity.get_in_reply_to_activity(activity)
end
end
def get_reply_to(%{data: %{"object" => object}}, _) do
if object["inReplyTo"] && object["inReplyTo"] != "" do
Activity.get_create_by_object_ap_id(object["inReplyTo"])
def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
object = Object.normalize(activity)
if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
else
nil
end
@ -276,8 +338,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
def get_visibility(object) do
public = "https://www.w3.org/ns/activitystreams#Public"
to = object["to"] || []
cc = object["cc"] || []
to = object.data["to"] || []
cc = object.data["cc"] || []
cond do
public in to ->
@ -298,25 +360,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
end
def render_content(%{"type" => "Video"} = object) do
with name when not is_nil(name) and name != "" <- object["name"] do
"<p><a href=\"#{object["id"]}\">#{name}</a></p>#{object["content"]}"
def render_content(%{data: %{"type" => "Video"}} = object) do
with name when not is_nil(name) and name != "" <- object.data["name"] do
"<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
else
_ -> object["content"] || ""
_ -> object.data["content"] || ""
end
end
def render_content(%{"type" => object_type} = object)
def render_content(%{data: %{"type" => object_type}} = object)
when object_type in ["Article", "Page"] do
with summary when not is_nil(summary) and summary != "" <- object["name"],
url when is_bitstring(url) <- object["url"] do
"<p><a href=\"#{url}\">#{summary}</a></p>#{object["content"]}"
with summary when not is_nil(summary) and summary != "" <- object.data["name"],
url when is_bitstring(url) <- object.data["url"] do
"<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
else
_ -> object["content"] || ""
_ -> object.data["content"] || ""
end
end
def render_content(object), do: object["content"] || ""
def render_content(object), do: object.data["content"] || ""
@doc """
Builds a dictionary tags.

View file

@ -90,7 +90,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
# Authenticated streams.
defp allow_request(stream, {"access_token", access_token}) when stream in @streams do
with %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token),
user = %User{} <- Repo.get(User, user_id) do
user = %User{} <- User.get_cached_by_id(user_id) do
{:ok, user}
else
_ -> {:error, 403}

View file

@ -13,32 +13,44 @@ defmodule Pleroma.Web.MediaProxy do
def url(url) do
config = Application.get_env(:pleroma, :media_proxy, [])
domain = URI.parse(url).host
if !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) do
url
else
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
# Must preserve `%2F` for compatibility with S3
# https://git.pleroma.social/pleroma/pleroma/issues/580
replacement = get_replacement(url, ":2F:")
# The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.
base64 =
cond do
!Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) ->
url
|> String.replace("%2F", replacement)
|> URI.decode()
|> URI.encode()
|> String.replace(replacement, "%2F")
|> Base.url_encode64(@base64_opts)
sig = :crypto.hmac(:sha, secret, base64)
sig64 = sig |> Base.url_encode64(@base64_opts)
Enum.any?(Pleroma.Config.get([:media_proxy, :whitelist]), fn pattern ->
String.equivalent?(domain, pattern)
end) ->
url
build_url(sig64, base64, filename(url))
true ->
encode_url(url)
end
end
def encode_url(url) do
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
# Must preserve `%2F` for compatibility with S3
# https://git.pleroma.social/pleroma/pleroma/issues/580
replacement = get_replacement(url, ":2F:")
# The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.
base64 =
url
|> String.replace("%2F", replacement)
|> URI.decode()
|> URI.encode()
|> String.replace(replacement, "%2F")
|> Base.url_encode64(@base64_opts)
sig = :crypto.hmac(:sha, secret, base64)
sig64 = sig |> Base.url_encode64(@base64_opts)
build_url(sig64, base64, filename(url))
end
def decode_url(sig, url) do
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
sig = Base.url_decode64!(sig, @base64_opts)

View file

@ -0,0 +1,13 @@
defmodule Pleroma.Web.Metadata.Providers.RelMe do
alias Pleroma.Web.Metadata.Providers.Provider
@behaviour Provider
@impl Provider
def build_tags(%{user: user}) do
(Floki.attribute(user.bio, "link[rel~=me]", "href") ++
Floki.attribute(user.bio, "a[rel~=me]", "href"))
|> Enum.map(fn link ->
{:link, [rel: "me", href: link], []}
end)
end
end

View file

@ -12,7 +12,7 @@ defmodule Pleroma.Web.Metadata.Utils do
# html content comes from DB already encoded, decode first and scrub after
|> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ")
|> HTML.get_cached_stripped_html_for_object(object, __MODULE__)
|> HTML.get_cached_stripped_html_for_activity(object, "metadata")
|> Formatter.demojify()
|> Formatter.truncate()
end

View file

@ -124,6 +124,9 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
end,
if Keyword.get(instance, :allow_relay) do
"relay"
end,
if Keyword.get(instance, :safe_dm_mentions) do
"safe_dm_mentions"
end
]
|> Enum.filter(& &1)

View file

@ -16,7 +16,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
schema "oauth_authorizations" do
field(:token, :string)
field(:scopes, {:array, :string}, default: [])
field(:valid_until, :naive_datetime)
field(:valid_until, :naive_datetime_usec)
field(:used, :boolean, default: false)
belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
belongs_to(:app, App)

View file

@ -6,11 +6,24 @@ defmodule Pleroma.Web.OAuth.FallbackController do
use Pleroma.Web, :controller
alias Pleroma.Web.OAuth.OAuthController
# No user/password
def call(conn, _) do
def call(conn, {:register, :generic_error}) do
conn
|> put_status(:internal_server_error)
|> put_flash(:error, "Unknown error, please check the details and try again.")
|> OAuthController.registration_details(conn.params)
end
def call(conn, {:register, _error}) do
conn
|> put_status(:unauthorized)
|> put_flash(:error, "Invalid Username/Password")
|> OAuthController.authorize(conn.params["authorization"])
|> OAuthController.registration_details(conn.params)
end
def call(conn, _error) do
conn
|> put_status(:unauthorized)
|> put_flash(:error, "Invalid Username/Password")
|> OAuthController.authorize(conn.params)
end
end

View file

@ -5,26 +5,57 @@
defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
alias Comeonin.Pbkdf2
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.Auth.Authenticator
alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
plug(:fetch_session)
plug(:fetch_flash)
action_fallback(Pleroma.Web.OAuth.FallbackController)
def authorize(conn, params) do
# Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg
def authorize(conn, %{"authorization" => _} = params) do
{auth_attrs, params} = Map.pop(params, "authorization")
authorize(conn, Map.merge(params, auth_attrs))
end
def authorize(%{assigns: %{token: %Token{} = token}} = conn, params) do
if ControllerHelper.truthy_param?(params["force_login"]) do
do_authorize(conn, params)
else
redirect_uri =
if is_binary(params["redirect_uri"]) do
params["redirect_uri"]
else
app = Repo.preload(token, :app).app
app.redirect_uris
|> String.split()
|> Enum.at(0)
end
redirect(conn, external: redirect_uri(conn, redirect_uri))
end
end
def authorize(conn, params), do: do_authorize(conn, params)
defp do_authorize(conn, params) do
app = Repo.get_by(App, client_id: params["client_id"])
available_scopes = (app && app.scopes) || []
scopes = oauth_scopes(params, nil) || available_scopes
# Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
render(conn, Authenticator.auth_template(), %{
response_type: params["response_type"],
client_id: params["client_id"],
@ -36,75 +67,83 @@ defmodule Pleroma.Web.OAuth.OAuthController do
})
end
def create_authorization(conn, %{
"authorization" =>
%{
"client_id" => client_id,
"redirect_uri" => redirect_uri
} = auth_params
def create_authorization(
conn,
%{"authorization" => _} = params,
opts \\ []
) do
with {:ok, auth} <- do_create_authorization(conn, params, opts[:user]) do
after_create_authorization(conn, auth, params)
else
error ->
handle_create_authorization_error(conn, error, params)
end
end
def after_create_authorization(conn, auth, %{
"authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
}) do
with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)},
%App{} = app <- Repo.get_by(App, client_id: client_id),
true <- redirect_uri in String.split(app.redirect_uris),
scopes <- oauth_scopes(auth_params, []),
{:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes},
# Note: `scope` param is intentionally not optional in this context
{:missing_scopes, false} <- {:missing_scopes, scopes == []},
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
redirect_uri =
if redirect_uri == "." do
# Special case: Local MastodonFE
mastodon_api_url(conn, :login)
redirect_uri = redirect_uri(conn, redirect_uri)
if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do
render(conn, "results.html", %{
auth: auth
})
else
connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?"
url = "#{redirect_uri}#{connector}"
url_params = %{:code => auth.token}
url_params =
if auth_attrs["state"] do
Map.put(url_params, :state, auth_attrs["state"])
else
redirect_uri
url_params
end
cond do
redirect_uri == "urn:ietf:wg:oauth:2.0:oob" ->
render(conn, "results.html", %{
auth: auth
})
url = "#{url}#{Plug.Conn.Query.encode(url_params)}"
true ->
connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?"
url = "#{redirect_uri}#{connector}"
url_params = %{:code => auth.token}
url_params =
if auth_params["state"] do
Map.put(url_params, :state, auth_params["state"])
else
url_params
end
url = "#{url}#{Plug.Conn.Query.encode(url_params)}"
redirect(conn, external: url)
end
else
{scopes_issue, _} when scopes_issue in [:unsupported_scopes, :missing_scopes] ->
conn
|> put_flash(:error, "Permissions not specified.")
|> put_status(:unauthorized)
|> authorize(auth_params)
{:auth_active, false} ->
conn
|> put_flash(:error, "Account confirmation pending.")
|> put_status(:forbidden)
|> authorize(auth_params)
error ->
Authenticator.handle_error(conn, error)
redirect(conn, external: url)
end
end
defp handle_create_authorization_error(
conn,
{scopes_issue, _},
%{"authorization" => _} = params
)
when scopes_issue in [:unsupported_scopes, :missing_scopes] do
# Per https://github.com/tootsuite/mastodon/blob/
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
conn
|> put_flash(:error, "This action is outside the authorized scopes")
|> put_status(:unauthorized)
|> authorize(params)
end
defp handle_create_authorization_error(
conn,
{:auth_active, false},
%{"authorization" => _} = params
) do
# Per https://github.com/tootsuite/mastodon/blob/
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76
conn
|> put_flash(:error, "Your login is missing a confirmed e-mail address")
|> put_status(:forbidden)
|> authorize(params)
end
defp handle_create_authorization_error(conn, error, %{"authorization" => _}) do
Authenticator.handle_error(conn, error)
end
def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
with %App{} = app <- get_app_from_request(conn, params),
fixed_token = fix_padding(params["code"]),
%Authorization{} = auth <-
Repo.get_by(Authorization, token: fixed_token, app_id: app.id),
%User{} = user <- User.get_cached_by_id(auth.user_id),
{:ok, token} <- Token.exchange_token(app, auth),
{:ok, inserted_at} <- DateTime.from_naive(token.inserted_at, "Etc/UTC") do
response = %{
@ -113,7 +152,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
refresh_token: token.refresh_token,
created_at: DateTime.to_unix(inserted_at),
expires_in: 60 * 10,
scope: Enum.join(token.scopes, " ")
scope: Enum.join(token.scopes, " "),
me: user.ap_id
}
json(conn, response)
@ -126,12 +166,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do
def token_exchange(
conn,
%{"grant_type" => "password", "username" => name, "password" => password} = params
%{"grant_type" => "password"} = params
) do
with %App{} = app <- get_app_from_request(conn, params),
%User{} = user <- User.get_by_nickname_or_email(name),
true <- Pbkdf2.checkpw(password, user.password_hash),
with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)},
%App{} = app <- get_app_from_request(conn, params),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:user_active, true} <- {:user_active, !user.info.deactivated},
scopes <- oauth_scopes(params, app.scopes),
[] <- scopes -- app.scopes,
true <- Enum.any?(scopes),
@ -142,15 +182,23 @@ defmodule Pleroma.Web.OAuth.OAuthController do
access_token: token.token,
refresh_token: token.refresh_token,
expires_in: 60 * 10,
scope: Enum.join(token.scopes, " ")
scope: Enum.join(token.scopes, " "),
me: user.ap_id
}
json(conn, response)
else
{:auth_active, false} ->
# Per https://github.com/tootsuite/mastodon/blob/
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76
conn
|> put_status(:forbidden)
|> json(%{error: "Account confirmation pending"})
|> json(%{error: "Your login is missing a confirmed e-mail address"})
{:user_active, false} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Your account is currently disabled"})
_error ->
put_status(conn, 400)
@ -182,6 +230,175 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end
@doc "Prepares OAuth request to provider for Ueberauth"
def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attrs}) do
scope =
oauth_scopes(auth_attrs, [])
|> Enum.join(" ")
state =
auth_attrs
|> Map.delete("scopes")
|> Map.put("scope", scope)
|> Poison.encode!()
params =
auth_attrs
|> Map.drop(~w(scope scopes client_id redirect_uri))
|> Map.put("state", state)
# Handing the request to Ueberauth
redirect(conn, to: o_auth_path(conn, :request, provider, params))
end
def request(conn, params) do
message =
if params["provider"] do
"Unsupported OAuth provider: #{params["provider"]}."
else
"Bad OAuth request."
end
conn
|> put_flash(:error, message)
|> redirect(to: "/")
end
def callback(%{assigns: %{ueberauth_failure: failure}} = conn, params) do
params = callback_params(params)
messages = for e <- Map.get(failure, :errors, []), do: e.message
message = Enum.join(messages, "; ")
conn
|> put_flash(:error, "Failed to authenticate: #{message}.")
|> redirect(external: redirect_uri(conn, params["redirect_uri"]))
end
def callback(conn, params) do
params = callback_params(params)
with {:ok, registration} <- Authenticator.get_registration(conn) do
user = Repo.preload(registration, :user).user
auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
if user do
create_authorization(
conn,
%{"authorization" => auth_attrs},
user: user
)
else
registration_params =
Map.merge(auth_attrs, %{
"nickname" => Registration.nickname(registration),
"email" => Registration.email(registration)
})
conn
|> put_session(:registration_id, registration.id)
|> registration_details(%{"authorization" => registration_params})
end
else
_ ->
conn
|> put_flash(:error, "Failed to set up user account.")
|> redirect(external: redirect_uri(conn, params["redirect_uri"]))
end
end
defp callback_params(%{"state" => state} = params) do
Map.merge(params, Poison.decode!(state))
end
def registration_details(conn, %{"authorization" => auth_attrs}) do
render(conn, "register.html", %{
client_id: auth_attrs["client_id"],
redirect_uri: auth_attrs["redirect_uri"],
state: auth_attrs["state"],
scopes: oauth_scopes(auth_attrs, []),
nickname: auth_attrs["nickname"],
email: auth_attrs["email"]
})
end
def register(conn, %{"authorization" => _, "op" => "connect"} = params) do
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
%Registration{} = registration <- Repo.get(Registration, registration_id),
{_, {:ok, auth}} <-
{:create_authorization, do_create_authorization(conn, params)},
%User{} = user <- Repo.preload(auth, :user).user,
{:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
conn
|> put_session_registration_id(nil)
|> after_create_authorization(auth, params)
else
{:create_authorization, error} ->
{:register, handle_create_authorization_error(conn, error, params)}
_ ->
{:register, :generic_error}
end
end
def register(conn, %{"authorization" => _, "op" => "register"} = params) do
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
%Registration{} = registration <- Repo.get(Registration, registration_id),
{:ok, user} <- Authenticator.create_from_registration(conn, registration) do
conn
|> put_session_registration_id(nil)
|> create_authorization(
params,
user: user
)
else
{:error, changeset} ->
message =
Enum.map(changeset.errors, fn {field, {error, _}} ->
"#{field} #{error}"
end)
|> Enum.join("; ")
message =
String.replace(
message,
"ap_id has already been taken",
"nickname has already been taken"
)
conn
|> put_status(:forbidden)
|> put_flash(:error, "Error: #{message}.")
|> registration_details(params)
_ ->
{:register, :generic_error}
end
end
defp do_create_authorization(
conn,
%{
"authorization" =>
%{
"client_id" => client_id,
"redirect_uri" => redirect_uri
} = auth_attrs
},
user \\ nil
) do
with {_, {:ok, %User{} = user}} <-
{:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
%App{} = app <- Repo.get_by(App, client_id: client_id),
true <- redirect_uri in String.split(app.redirect_uris),
scopes <- oauth_scopes(auth_attrs, []),
{:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes},
# Note: `scope` param is intentionally not optional in this context
{:missing_scopes, false} <- {:missing_scopes, scopes == []},
{:auth_active, true} <- {:auth_active, User.auth_active?(user)} do
Authorization.create_authorization(app, user, scopes)
end
end
# XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be
# decoding it. Investigate sometime.
defp fix_padding(token) do
@ -214,4 +431,14 @@ defmodule Pleroma.Web.OAuth.OAuthController do
nil
end
end
# Special case: Local MastodonFE
defp redirect_uri(conn, "."), do: mastodon_api_url(conn, :login)
defp redirect_uri(_conn, redirect_uri), do: redirect_uri
defp get_session_registration_id(conn), do: get_session(conn, :registration_id)
defp put_session_registration_id(conn, registration_id),
do: put_session(conn, :registration_id, registration_id)
end

View file

@ -17,7 +17,7 @@ defmodule Pleroma.Web.OAuth.Token do
field(:token, :string)
field(:refresh_token, :string)
field(:scopes, {:array, :string}, default: [])
field(:valid_until, :naive_datetime)
field(:valid_until, :naive_datetime_usec)
belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
belongs_to(:app, App)
@ -27,7 +27,7 @@ defmodule Pleroma.Web.OAuth.Token do
def exchange_token(app, auth) do
with {:ok, auth} <- Authorization.use_token(auth),
true <- auth.app_id == app.id do
create_token(app, Repo.get(User, auth.user_id), auth.scopes)
create_token(app, User.get_cached_by_id(auth.user_id), auth.scopes)
end
end

View file

@ -54,23 +54,16 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
end)
end
defp get_links(%{local: true, data: data}) do
defp get_links(%{local: true}, %{"id" => object_id}) do
h = fn str -> [to_charlist(str)] end
[
{:link, [type: ['application/atom+xml'], href: h.(data["object"]["id"]), rel: 'self'], []},
{:link, [type: ['text/html'], href: h.(data["object"]["id"]), rel: 'alternate'], []}
{:link, [type: ['application/atom+xml'], href: h.(object_id), rel: 'self'], []},
{:link, [type: ['text/html'], href: h.(object_id), rel: 'alternate'], []}
]
end
defp get_links(%{
local: false,
data: %{
"object" => %{
"external_url" => external_url
}
}
}) do
defp get_links(%{local: false}, %{"external_url" => external_url}) do
h = fn str -> [to_charlist(str)] end
[
@ -78,7 +71,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
]
end
defp get_links(_activity), do: []
defp get_links(_activity, _object_data), do: []
defp get_emoji_links(emojis) do
Enum.map(emojis, fn {emoji, file} ->
@ -88,14 +81,16 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
def to_simple_form(activity, user, with_author \\ false)
def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user, with_author) do
def to_simple_form(%{data: %{"type" => "Create"}} = activity, user, with_author) do
h = fn str -> [to_charlist(str)] end
updated_at = activity.data["object"]["published"]
inserted_at = activity.data["object"]["published"]
object = Object.normalize(activity)
updated_at = object.data["published"]
inserted_at = object.data["published"]
attachments =
Enum.map(activity.data["object"]["attachment"] || [], fn attachment ->
Enum.map(object.data["attachment"] || [], fn attachment ->
url = hd(attachment["url"])
{:link,
@ -108,7 +103,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
mentions = activity.recipients |> get_mentions
categories =
(activity.data["object"]["tag"] || [])
(object.data["tag"] || [])
|> Enum.map(fn tag ->
if is_binary(tag) do
{:category, [term: to_charlist(tag)], []}
@ -118,11 +113,11 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
end)
|> Enum.filter(& &1)
emoji_links = get_emoji_links(activity.data["object"]["emoji"] || %{})
emoji_links = get_emoji_links(object.data["emoji"] || %{})
summary =
if activity.data["object"]["summary"] do
[{:summary, [], h.(activity.data["object"]["summary"])}]
if object.data["summary"] do
[{:summary, [], h.(object.data["summary"])}]
else
[]
end
@ -131,10 +126,9 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/post']},
# For notes, federate the object id.
{:id, h.(activity.data["object"]["id"])},
{:id, h.(object.data["id"])},
{:title, ['New note by #{user.nickname}']},
{:content, [type: 'html'],
h.(activity.data["object"]["content"] |> String.replace(~r/[\n\r]/, ""))},
{:content, [type: 'html'], h.(object.data["content"] |> String.replace(~r/[\n\r]/, ""))},
{:published, h.(inserted_at)},
{:updated, h.(updated_at)},
{:"ostatus:conversation", [ref: h.(activity.data["context"])],
@ -142,7 +136,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []}
] ++
summary ++
get_links(activity) ++
get_links(activity, object.data) ++
categories ++ attachments ++ in_reply_to ++ author ++ mentions ++ emoji_links
end

View file

@ -106,15 +106,16 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
# TODO: Clean this up a bit.
def handle_note(entry, doc \\ nil) do
with id <- XML.string_from_xpath("//id", entry),
activity when is_nil(activity) <- Activity.get_create_by_object_ap_id(id),
activity when is_nil(activity) <- Activity.get_create_by_object_ap_id_with_object(id),
[author] <- :xmerl_xpath.string('//author[1]', doc),
{:ok, actor} <- OStatus.find_make_or_update_user(author),
content_html <- OStatus.get_content(entry),
cw <- OStatus.get_cw(entry),
in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry),
in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to),
in_reply_to <-
(in_reply_to_activity && in_reply_to_activity.data["object"]["id"]) || in_reply_to,
in_reply_to_object <-
(in_reply_to_activity && Object.normalize(in_reply_to_activity)) || nil,
in_reply_to <- (in_reply_to_object && in_reply_to_object.data["id"]) || in_reply_to,
attachments <- OStatus.get_attachments(entry),
context <- get_context(entry, in_reply_to),
tags <- OStatus.get_tags(entry),

View file

@ -23,8 +23,8 @@ defmodule Pleroma.Web.OStatus do
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Websub
def is_representable?(%Activity{data: data}) do
object = Object.normalize(data["object"])
def is_representable?(%Activity{} = activity) do
object = Object.normalize(activity)
cond do
is_nil(object) ->
@ -119,7 +119,7 @@ defmodule Pleroma.Web.OStatus do
def make_share(entry, doc, retweeted_activity) do
with {:ok, actor} <- find_make_or_update_user(doc),
%Object{} = object <- Object.normalize(retweeted_activity.data["object"]),
%Object{} = object <- Object.normalize(retweeted_activity),
id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do
{:ok, activity}
@ -137,7 +137,7 @@ defmodule Pleroma.Web.OStatus do
def make_favorite(entry, doc, favorited_activity) do
with {:ok, actor} <- find_make_or_update_user(doc),
%Object{} = object <- Object.normalize(favorited_activity.data["object"]),
%Object{} = object <- Object.normalize(favorited_activity),
id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do
{:ok, activity}
@ -159,7 +159,7 @@ defmodule Pleroma.Web.OStatus do
Logger.debug("Trying to get entry from db")
with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
%Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
{:ok, activity}
else
_ ->
@ -294,7 +294,7 @@ defmodule Pleroma.Web.OStatus do
}
with false <- update,
%User{} = user <- User.get_by_ap_id(data.ap_id) do
%User{} = user <- User.get_cached_by_ap_id(data.ap_id) do
{:ok, user}
else
_e -> User.insert_or_update_user(data)

View file

@ -102,7 +102,8 @@ defmodule Pleroma.Web.OStatus.OStatusController do
ActivityPubController.call(conn, :object)
else
with id <- o_status_url(conn, :object, uuid),
{_, %Activity{} = activity} <- {:activity, Activity.get_create_by_object_ap_id(id)},
{_, %Activity{} = activity} <-
{:activity, Activity.get_create_by_object_ap_id_with_object(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case get_format(conn) do
@ -148,13 +149,13 @@ defmodule Pleroma.Web.OStatus.OStatusController do
end
def notice(conn, %{"id" => id}) do
with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id(id)},
with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case format = get_format(conn) do
"html" ->
if activity.data["type"] == "Create" do
%Object{} = object = Object.normalize(activity.data["object"])
%Object{} = object = Object.normalize(activity)
Fallback.RedirectController.redirector_with_meta(conn, %{
activity_id: activity.id,
@ -191,9 +192,9 @@ defmodule Pleroma.Web.OStatus.OStatusController do
# Returns an HTML embedded <audio> or <video> player suitable for embed iframes.
def notice_player(conn, %{"id" => id}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.is_public?(activity),
%Object{} = object <- Object.normalize(activity.data["object"]),
%Object{} = object <- Object.normalize(activity),
%{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do
conn
@ -219,7 +220,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
%Activity{data: %{"type" => "Create"}} = activity,
_user
) do
object = Object.normalize(activity.data["object"])
object = Object.normalize(activity)
conn
|> put_resp_header("content-type", "application/activity+json")

View file

@ -19,10 +19,12 @@ defmodule Pleroma.Web.Push.Impl do
@types ["Create", "Follow", "Announce", "Like"]
@doc "Performs sending notifications for user subscriptions"
@spec perform_send(Notification.t()) :: list(any)
def perform_send(
%{activity: %{data: %{"type" => activity_type}, id: activity_id}, user_id: user_id} =
notif
@spec perform(Notification.t()) :: list(any) | :error
def perform(
%{
activity: %{data: %{"type" => activity_type}, id: activity_id} = activity,
user_id: user_id
} = notif
)
when activity_type in @types do
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
@ -30,13 +32,14 @@ defmodule Pleroma.Web.Push.Impl do
type = Activity.mastodon_notification_type(notif.activity)
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor)
object = Object.normalize(activity)
for subscription <- fetch_subsriptions(user_id),
get_in(subscription.data, ["alerts", type]) do
%{
title: format_title(notif),
access_token: subscription.token.token,
body: format_body(notif, actor),
body: format_body(notif, actor, object),
notification_id: notif.id,
notification_type: type,
icon: avatar_url,
@ -50,7 +53,7 @@ defmodule Pleroma.Web.Push.Impl do
end
end
def perform_send(_) do
def perform(_) do
Logger.warn("Unknown notification type")
:error
end
@ -95,25 +98,25 @@ defmodule Pleroma.Web.Push.Impl do
end
def format_body(
%{activity: %{data: %{"type" => "Create", "object" => %{"content" => content}}}},
actor
%{activity: %{data: %{"type" => "Create"}}},
actor,
%{data: %{"content" => content}}
) do
"@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
end
def format_body(
%{activity: %{data: %{"type" => "Announce", "object" => activity_id}}},
actor
%{activity: %{data: %{"type" => "Announce"}}},
actor,
%{data: %{"content" => content}}
) do
%Activity{data: %{"object" => %{"id" => object_id}}} = Activity.get_by_ap_id(activity_id)
%Object{data: %{"content" => content}} = Object.get_by_ap_id(object_id)
"@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
end
def format_body(
%{activity: %{data: %{"type" => type}}},
actor
actor,
_object
)
when type in ["Follow", "Like"] do
case type do

View file

@ -3,18 +3,20 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Push do
use GenServer
alias Pleroma.Web.Push.Impl
require Logger
##############
# Client API #
##############
def init do
unless enabled() do
Logger.warn("""
VAPID key pair is not found. If you wish to enabled web push, please run
def start_link do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
mix web_push.gen.keypair
and add the resulting output to your configuration file.
""")
end
end
def vapid_config do
@ -30,35 +32,5 @@ defmodule Pleroma.Web.Push do
end
def send(notification),
do: GenServer.cast(__MODULE__, {:send, notification})
####################
# Server Callbacks #
####################
@impl true
def init(:ok) do
if enabled() do
{:ok, nil}
else
Logger.warn("""
VAPID key pair is not found. If you wish to enabled web push, please run
mix web_push.gen.keypair
and add the resulting output to your configuration file.
""")
:ignore
end
end
@impl true
def handle_cast({:send, notification}, state) do
if enabled() do
Impl.perform_send(notification)
end
{:noreply, state}
end
do: PleromaJobQueue.enqueue(:web_push, Impl, [notification])
end

View file

@ -6,7 +6,8 @@ defmodule Pleroma.Web.RelMe do
@hackney_options [
pool: :media,
recv_timeout: 2_000,
max_body: 2_000_000
max_body: 2_000_000,
with_body: true
]
if Mix.env() == :test do

View file

@ -21,9 +21,9 @@ defmodule Pleroma.Web.RichMedia.Helpers do
defp validate_page_url(%URI{}), do: :ok
defp validate_page_url(_), do: :error
def fetch_data_for_activity(%Activity{} = activity) do
def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) do
with true <- Pleroma.Config.get([:rich_media, :enabled]),
%Object{} = object <- Object.normalize(activity.data["object"]),
%Object{} = object <- Object.normalize(activity),
{:ok, page_url} <- HTML.extract_first_external_url(object, object.data["content"]),
:ok <- validate_page_url(page_url),
{:ok, rich_media} <- Parser.parse(page_url) do
@ -32,4 +32,6 @@ defmodule Pleroma.Web.RichMedia.Helpers do
_ -> %{}
end
end
def fetch_data_for_activity(_), do: %{}
end

View file

@ -12,7 +12,8 @@ defmodule Pleroma.Web.RichMedia.Parser do
@hackney_options [
pool: :media,
recv_timeout: 2_000,
max_body: 2_000_000
max_body: 2_000_000,
with_body: true
]
def parse(nil), do: {:error, "No URL provided"}

View file

@ -5,6 +5,16 @@
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
end
pipeline :oauth do
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
end
pipeline :api do
plug(:accepts, ["json"])
plug(:fetch_session)
@ -105,10 +115,6 @@ defmodule Pleroma.Web.Router do
plug(:accepts, ["json", "xml"])
end
pipeline :oauth do
plug(:accepts, ["html", "json"])
end
pipeline :pleroma_api do
plug(:accepts, ["html", "json"])
end
@ -129,6 +135,7 @@ defmodule Pleroma.Web.Router do
post("/password_reset", UtilController, :password_reset)
get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha)
get("/healthcheck", UtilController, :healthcheck)
end
scope "/api/pleroma", Pleroma.Web do
@ -139,7 +146,12 @@ defmodule Pleroma.Web.Router do
scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through([:admin_api, :oauth_write])
post("/user/follow", AdminAPIController, :user_follow)
post("/user/unfollow", AdminAPIController, :user_unfollow)
get("/users", AdminAPIController, :list_users)
get("/users/:nickname", AdminAPIController, :user_show)
delete("/user", AdminAPIController, :user_delete)
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
post("/user", AdminAPIController, :user_create)
@ -157,6 +169,8 @@ defmodule Pleroma.Web.Router do
delete("/relay", AdminAPIController, :relay_unfollow)
get("/invite_token", AdminAPIController, :get_invite_token)
get("/invites", AdminAPIController, :invites)
post("/revoke_invite", AdminAPIController, :revoke_invite)
post("/email_invite", AdminAPIController, :email_invite)
get("/password_reset", AdminAPIController, :get_password_reset)
@ -182,6 +196,7 @@ defmodule Pleroma.Web.Router do
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
put("/notification_settings", UtilController, :update_notificaton_settings)
end
scope [] do
@ -190,13 +205,33 @@ defmodule Pleroma.Web.Router do
post("/blocks_import", UtilController, :blocks_import)
post("/follow_import", UtilController, :follow_import)
end
scope [] do
pipe_through(:oauth_read)
post("/notifications/read", UtilController, :notifications_read)
end
end
scope "/oauth", Pleroma.Web.OAuth do
get("/authorize", OAuthController, :authorize)
scope [] do
pipe_through(:oauth)
get("/authorize", OAuthController, :authorize)
end
post("/authorize", OAuthController, :create_authorization)
post("/token", OAuthController, :token_exchange)
post("/revoke", OAuthController, :token_revoke)
get("/registration_details", OAuthController, :registration_details)
scope [] do
pipe_through(:browser)
get("/prepare_request", OAuthController, :prepare_request)
get("/:provider", OAuthController, :request)
get("/:provider/callback", OAuthController, :callback)
post("/register", OAuthController, :register)
end
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
@ -208,9 +243,9 @@ defmodule Pleroma.Web.Router do
get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials)
get("/accounts/relationships", MastodonAPIController, :relationships)
get("/accounts/search", MastodonAPIController, :account_search)
get("/accounts/:id/lists", MastodonAPIController, :account_lists)
get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
get("/follow_requests", MastodonAPIController, :follow_requests)
get("/blocks", MastodonAPIController, :blocks)
@ -226,6 +261,10 @@ defmodule Pleroma.Web.Router do
post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
get("/notifications", MastodonAPIController, :notifications)
get("/notifications/:id", MastodonAPIController, :get_notification)
delete("/notifications/destroy_multiple", MastodonAPIController, :destroy_multiple)
get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses)
get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status)
get("/lists", MastodonAPIController, :get_lists)
get("/lists/:id", MastodonAPIController, :get_list)
@ -261,6 +300,9 @@ defmodule Pleroma.Web.Router do
post("/statuses/:id/mute", MastodonAPIController, :mute_conversation)
post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation)
put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)
delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status)
post("/media", MastodonAPIController, :upload)
put("/media/:id", MastodonAPIController, :update_media)
@ -298,6 +340,9 @@ defmodule Pleroma.Web.Router do
post("/domain_blocks", MastodonAPIController, :block_domain)
delete("/domain_blocks", MastodonAPIController, :unblock_domain)
post("/pleroma/accounts/:id/subscribe", MastodonAPIController, :subscribe)
post("/pleroma/accounts/:id/unsubscribe", MastodonAPIController, :unsubscribe)
end
scope [] do
@ -322,6 +367,7 @@ defmodule Pleroma.Web.Router do
get("/instance", MastodonAPIController, :masto_instance)
get("/instance/peers", MastodonAPIController, :peers)
post("/apps", MastodonAPIController, :create_app)
get("/apps/verify_credentials", MastodonAPIController, :verify_app_credentials)
get("/custom_emojis", MastodonAPIController, :custom_emojis)
get("/statuses/:id/card", MastodonAPIController, :status_card)
@ -331,6 +377,8 @@ defmodule Pleroma.Web.Router do
get("/trends", MastodonAPIController, :empty_array)
get("/accounts/search", MastodonAPIController, :account_search)
scope [] do
pipe_through(:oauth_read_or_unauthenticated)
@ -347,6 +395,8 @@ defmodule Pleroma.Web.Router do
get("/accounts/:id", MastodonAPIController, :user)
get("/search", MastodonAPIController, :search)
get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)
end
end

View file

@ -8,9 +8,10 @@ defmodule Pleroma.Web.Streamer do
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.MastodonAPI.NotificationView
@keepalive_interval :timer.seconds(30)
@ -80,7 +81,7 @@ defmodule Pleroma.Web.Streamer do
_ ->
Pleroma.List.get_lists_from_activity(item)
|> Enum.filter(fn list ->
owner = Repo.get(User, list.user_id)
owner = User.get_cached_by_id(list.user_id)
Visibility.visible_for_user?(item, owner)
end)
@ -106,10 +107,10 @@ defmodule Pleroma.Web.Streamer do
%{
event: "notification",
payload:
Pleroma.Web.MastodonAPI.MastodonAPIController.render_notification(
socket.assigns["user"],
item
)
NotificationView.render("show.json", %{
notification: item,
for: socket.assigns["user"]
})
|> Jason.encode!()
}
|> Jason.encode!()
@ -198,10 +199,12 @@ defmodule Pleroma.Web.Streamer do
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
blocks = user.info.blocks || []
mutes = user.info.mutes || []
reblog_mutes = user.info.muted_reblogs || []
parent = Object.normalize(item.data["object"])
parent = Object.normalize(item)
unless is_nil(parent) or item.actor in blocks or item.actor in mutes or
item.actor in reblog_mutes or not ActivityPub.contain_activity(item, user) or
parent.data["actor"] in blocks or parent.data["actor"] in mutes do
send(socket.transport_pid, {:text, represent_update(item, user)})
end
@ -232,7 +235,8 @@ defmodule Pleroma.Web.Streamer do
blocks = user.info.blocks || []
mutes = user.info.mutes || []
unless item.actor in blocks or item.actor in mutes do
unless item.actor in blocks or item.actor in mutes or
not ActivityPub.contain_activity(item, user) do
send(socket.transport_pid, {:text, represent_update(item, user)})
end
else

View file

@ -179,6 +179,17 @@
flex-basis: 50%;
}
}
.form-row {
display: flex;
}
.form-row > label {
text-align: left;
line-height: 47px;
flex: 1;
}
.form-row > input {
flex: 2;
}
</style>
</head>
<body>

View file

@ -0,0 +1,13 @@
<div class="scopes-input">
<%= label @form, :scope, "Permissions" %>
<div class="scopes">
<%= for scope <- @available_scopes do %>
<%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %>
<div class="scope">
<%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %>
<%= label @form, :"scope_#{scope}", String.capitalize(scope) %>
</div>
<% end %>
</div>
</div>

View file

@ -0,0 +1,13 @@
<h2>Sign in with external provider</h2>
<%= form_for @conn, o_auth_path(@conn, :prepare_request), [as: "authorization", method: "get"], fn f -> %>
<%= render @view_module, "_scopes.html", Map.put(assigns, :form, f) %>
<%= hidden_input f, :client_id, value: @client_id %>
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
<%= hidden_input f, :state, value: @state %>
<%= for strategy <- Pleroma.Config.oauth_consumer_strategies() do %>
<%= submit "Sign in with #{String.capitalize(strategy)}", name: "provider", value: strategy %>
<% end %>
<% end %>

View file

@ -0,0 +1,42 @@
<%= if get_flash(@conn, :info) do %>
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<% end %>
<%= if get_flash(@conn, :error) do %>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<% end %>
<h2>Registration Details</h2>
<p>If you'd like to register a new account, please provide the details below.</p>
<%= form_for @conn, o_auth_path(@conn, :register), [as: "authorization"], fn f -> %>
<div class="input">
<%= label f, :nickname, "Nickname" %>
<%= text_input f, :nickname, value: @nickname %>
</div>
<div class="input">
<%= label f, :email, "Email" %>
<%= text_input f, :email, value: @email %>
</div>
<%= submit "Proceed as new user", name: "op", value: "register" %>
<p>Alternatively, sign in to connect to existing account.</p>
<div class="input">
<%= label f, :name, "Name or email" %>
<%= text_input f, :name %>
</div>
<div class="input">
<%= label f, :password, "Password" %>
<%= password_input f, :password %>
</div>
<%= submit "Proceed as existing user", name: "op", value: "connect" %>
<%= hidden_input f, :client_id, value: @client_id %>
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
<%= hidden_input f, :scope, value: Enum.join(@scopes, " ") %>
<%= hidden_input f, :state, value: @state %>
<% end %>

View file

@ -4,7 +4,9 @@
<%= if get_flash(@conn, :error) do %>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<% end %>
<h2>OAuth Authorization</h2>
<%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>
<div class="input">
<%= label f, :name, "Name or email" %>
@ -14,22 +16,16 @@
<%= label f, :password, "Password" %>
<%= password_input f, :password %>
</div>
<div class="scopes-input">
<%= label f, :scope, "Permissions" %>
<div class="scopes">
<%= for scope <- @available_scopes do %>
<%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %>
<div class="scope">
<%= checkbox f, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %>
<%= label f, :"scope_#{scope}", String.capitalize(scope) %>
</div>
<% end %>
</div>
</div>
<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
<%= hidden_input f, :client_id, value: @client_id %>
<%= hidden_input f, :response_type, value: @response_type %>
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
<%= hidden_input f, :state, value: @state%>
<%= hidden_input f, :state, value: @state %>
<%= submit "Authorize" %>
<% end %>
<%= if Pleroma.Config.oauth_consumer_enabled?() do %>
<%= render @view_module, Pleroma.Web.Auth.Authenticator.oauth_consumer_template(), assigns %>
<% end %>

View file

@ -1,12 +1,13 @@
<h2>Password Reset for <%= @user.nickname %></h2>
<%= form_for @conn, util_path(@conn, :password_reset), [as: "data"], fn f -> %>
<%= label f, :password, "Password" %>
<%= password_input f, :password %>
<br>
<%= label f, :password_confirmation, "Confirmation" %>
<%= password_input f, :password_confirmation %>
<br>
<%= hidden_input f, :token, value: @token.token %>
<%= submit "Reset" %>
<div class="form-row">
<%= label f, :password, "Password" %>
<%= password_input f, :password %>
</div>
<div class="form-row">
<%= label f, :password_confirmation, "Confirmation" %>
<%= password_input f, :password_confirmation %>
</div>
<%= hidden_input f, :token, value: @token.token %>
<%= submit "Reset" %>
<% end %>

View file

@ -8,7 +8,9 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
require Logger
alias Comeonin.Pbkdf2
alias Pleroma.Activity
alias Pleroma.Emoji
alias Pleroma.Notification
alias Pleroma.PasswordResetToken
alias Pleroma.Repo
alias Pleroma.User
@ -20,7 +22,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
def show_password_reset(conn, %{"token" => token}) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
%User{} = user <- Repo.get(User, token.user_id) do
%User{} = user <- User.get_cached_by_id(token.user_id) do
render(conn, "password_reset.html", %{
token: token,
user: user
@ -72,36 +74,52 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
def remote_follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do
{err, followee} = OStatus.find_or_make_user(acct)
avatar = User.avatar_url(followee)
name = followee.nickname
id = followee.id
if !!user do
conn
|> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id})
if is_status?(acct) do
{:ok, object} = Pleroma.Object.Fetcher.fetch_object_from_id(acct)
%Activity{id: activity_id} = Activity.get_create_by_object_ap_id(object.data["id"])
redirect(conn, to: "/notice/#{activity_id}")
else
conn
|> render("follow_login.html", %{
error: false,
acct: acct,
avatar: avatar,
name: name,
id: id
})
{err, followee} = OStatus.find_or_make_user(acct)
avatar = User.avatar_url(followee)
name = followee.nickname
id = followee.id
if !!user do
conn
|> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id})
else
conn
|> render("follow_login.html", %{
error: false,
acct: acct,
avatar: avatar,
name: name,
id: id
})
end
end
end
defp is_status?(acct) do
case Pleroma.Object.Fetcher.fetch_and_contain_remote_object_from_id(acct) do
{:ok, %{"type" => type}} when type in ["Article", "Note", "Video", "Page", "Question"] ->
true
_ ->
false
end
end
def do_remote_follow(conn, %{
"authorization" => %{"name" => username, "password" => password, "id" => id}
}) do
followee = Repo.get(User, id)
followee = User.get_cached_by_id(id)
avatar = User.avatar_url(followee)
name = followee.nickname
with %User{} = user <- User.get_cached_by_nickname(username),
true <- Pbkdf2.checkpw(password, user.password_hash),
%User{} = _followed <- Repo.get(User, id),
%User{} = _followed <- User.get_cached_by_id(id),
{:ok, follower} <- User.follow(user, followee),
{:ok, _activity} <- ActivityPub.follow(follower, followee) do
conn
@ -123,7 +141,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do
with %User{} = followee <- Repo.get(User, id),
with %User{} = followee <- User.get_cached_by_id(id),
{:ok, follower} <- User.follow(user, followee),
{:ok, _activity} <- ActivityPub.follow(follower, followee) do
conn
@ -142,6 +160,17 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
with {:ok, _} <- Notification.read_one(user, notification_id) do
json(conn, %{status: "success"})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def config(conn, _params) do
instance = Pleroma.Config.get(:instance)
instance_fe = Pleroma.Config.get(:fe)
@ -185,7 +214,9 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
vapidPublicKey: vapid_public_key,
accountActivationRequired:
if(Keyword.get(instance, :account_activation_required, false), do: "1", else: "0"),
invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0")
invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0"),
safeDMMentionsEnabled:
if(Pleroma.Config.get([:instance, :safe_dm_mentions]), do: "1", else: "0")
}
pleroma_fe =
@ -252,7 +283,20 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
def emoji(conn, _params) do
json(conn, Enum.into(Emoji.get_all(), %{}))
emoji =
Emoji.get_all()
|> Enum.map(fn {short_code, path, tags} ->
{short_code, %{image_url: path, tags: tags}}
end)
|> Enum.into(%{})
json(conn, emoji)
end
def update_notificaton_settings(%{assigns: %{user: user}} = conn, params) do
with {:ok, _} <- User.update_notification_settings(user, params) do
json(conn, %{status: "success"})
end
end
def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
@ -260,7 +304,12 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
def follow_import(%{assigns: %{user: follower}} = conn, %{"list" => list}) do
with followed_identifiers <- String.split(list),
with lines <- String.split(list, "\n"),
followed_identifiers <-
Enum.map(lines, fn line ->
String.split(line, ",") |> List.first()
end)
|> List.delete("Account address"),
{:ok, _} = Task.start(fn -> User.follow_import(follower, followed_identifiers) end) do
json(conn, "job started")
end
@ -314,4 +363,22 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
def captcha(conn, _params) do
json(conn, Pleroma.Captcha.new())
end
def healthcheck(conn, _params) do
info =
if Pleroma.Config.get([:instance, :healthcheck]) do
Pleroma.Healthcheck.system_info()
else
%{}
end
conn =
if info[:healthy] do
conn
else
Plug.Conn.put_status(conn, :service_unavailable)
end
json(conn, info)
end
end

View file

@ -1,15 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# FIXME: Remove this module?
# THIS MODULE IS DEPRECATED! DON'T USE IT!
# USE THE Pleroma.Web.TwitterAPI.Views.ActivityView MODULE!
defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
def to_map(activity, opts) do
Pleroma.Web.TwitterAPI.ActivityView.render(
"activity.json",
Map.put(opts, :activity, activity)
)
end
end

View file

@ -4,11 +4,10 @@
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
alias Pleroma.Activity
alias Pleroma.Mailer
alias Pleroma.Object
alias Pleroma.Emails.Mailer
alias Pleroma.Emails.UserEmail
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserEmail
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
@ -21,7 +20,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
end
def delete(%User{} = user, id) do
with %Activity{data: %{"type" => _type}} <- Repo.get(Activity, id),
with %Activity{data: %{"type" => _type}} <- Activity.get_by_id(id),
{:ok, activity} <- CommonAPI.delete(id, user) do
{:ok, activity}
end
@ -130,7 +129,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
end
def register_user(params) do
token_string = params["token"]
token = params["token"]
params = %{
nickname: params["nickname"],
@ -164,36 +163,49 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
{:error, %{error: Jason.encode!(%{captcha: [error]})}}
else
registrations_open = Pleroma.Config.get([:instance, :registrations_open])
registration_process(registrations_open, params, token)
end
end
# no need to query DB if registration is open
token =
unless registrations_open || is_nil(token_string) do
Repo.get_by(UserInviteToken, %{token: token_string})
end
cond do
registrations_open || (!is_nil(token) && !token.used) ->
changeset = User.register_changeset(%User{}, params)
with {:ok, user} <- User.register(changeset) do
!registrations_open && UserInviteToken.mark_as_used(token.token)
{:ok, user}
else
{:error, changeset} ->
errors =
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|> Jason.encode!()
{:error, %{error: errors}}
end
!registrations_open && is_nil(token) ->
{:error, "Invalid token"}
!registrations_open && token.used ->
{:error, "Expired token"}
defp registration_process(registration_open, params, token)
when registration_open == false or is_nil(registration_open) do
invite =
unless is_nil(token) do
Repo.get_by(UserInviteToken, %{token: token})
end
valid_invite? = invite && UserInviteToken.valid_invite?(invite)
case invite do
nil ->
{:error, "Invalid token"}
invite when valid_invite? ->
UserInviteToken.update_usage!(invite)
create_user(params)
_ ->
{:error, "Expired token"}
end
end
defp registration_process(true, params, _token) do
create_user(params)
end
defp create_user(params) do
changeset = User.register_changeset(%User{}, params)
case User.register(changeset) do
{:ok, user} ->
{:ok, user}
{:error, changeset} ->
errors =
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|> Jason.encode!()
{:error, %{error: errors}}
end
end
@ -228,12 +240,9 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
end
%{"screen_name" => nickname} ->
case target = Repo.get_by(User, nickname: nickname) do
nil ->
{:error, "No user with such screen_name"}
_ ->
{:ok, target}
case User.get_cached_by_nickname(nickname) do
nil -> {:error, "No user with such screen_name"}
target -> {:ok, target}
end
_ ->
@ -257,6 +266,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
defp parse_int(_, default), do: default
# TODO: unify the search query with MastoAPI one and do only pagination here
def search(_user, %{"q" => query} = params) do
limit = parse_int(params["rpp"], 20)
page = parse_int(params["page"], 1)
@ -264,13 +274,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
q =
from(
a in Activity,
[a, o] in Activity.with_preloaded_object(Activity),
where: fragment("?->>'type' = 'Create'", a.data),
where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
where:
fragment(
"to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
a.data,
"to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
o.data,
^query
),
limit: ^limit,
@ -282,35 +292,6 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
_activities = Repo.all(q)
end
# DEPRECATED mostly, context objects are now created at insertion time.
def context_to_conversation_id(context) do
with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
id
else
_e ->
changeset = Object.context_mapping(context)
case Repo.insert(changeset) do
{:ok, %{id: id}} ->
id
# This should be solved by an upsert, but it seems ecto
# has problems accessing the constraint inside the jsonb.
{:error, _} ->
Object.get_cached_by_ap_id(context).id
end
end
end
def conversation_id_to_context(id) do
with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
context
else
_e ->
{:error, "No such conversation"}
end
end
def get_external_profile(for_user, uri) do
with {:ok, %User{} = user} <- User.get_or_fetch(uri) do
{:ok, UserView.render("show.json", %{user: user, for: for_user})}

View file

@ -16,6 +16,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.ActivityView
alias Pleroma.Web.TwitterAPI.NotificationView
@ -269,7 +270,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
with %Activity{} = activity <- Activity.get_by_id(id),
true <- Visibility.visible_for_user?(activity, user) do
conn
|> put_view(ActivityView)
@ -278,7 +279,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with context when is_binary(context) <- TwitterAPI.conversation_id_to_context(id),
with context when is_binary(context) <- Utils.conversation_id_to_context(id),
activities <-
ActivityPub.fetch_activities_for_context(context, %{
"blocking_user" => user,
@ -341,7 +342,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def get_by_id_or_ap_id(id) do
activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id)
activity = Activity.get_by_id(id) || Activity.get_create_by_object_ap_id(id)
if activity.data["type"] == "Create" do
activity
@ -433,7 +434,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
with %User{} = user <- Repo.get(User, uid),
with %User{} = user <- User.get_cached_by_id(uid),
true <- user.local,
true <- user.info.confirmation_pending,
true <- user.info.confirmation_token == token,
@ -586,7 +587,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
def approve_friend_request(conn, %{"user_id" => uid} = _params) do
with followed <- conn.assigns[:user],
%User{} = follower <- Repo.get(User, uid),
%User{} = follower <- User.get_cached_by_id(uid),
{:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
conn
|> put_view(UserView)
@ -598,7 +599,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
def deny_friend_request(conn, %{"user_id" => uid} = _params) do
with followed <- conn.assigns[:user],
%User{} = follower <- Repo.get(User, uid),
%User{} = follower <- User.get_cached_by_id(uid),
{:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
conn
|> put_view(UserView)
@ -631,7 +632,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
defp build_info_cng(user, params) do
info_params =
["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]
["no_rich_text", "locked", "hide_followers", "hide_follows", "hide_favorites", "show_role"]
|> Enum.reduce(%{}, fn key, res ->
if value = params[key] do
Map.put(res, key, value == "true")

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