Merge develop

This commit is contained in:
Roman Chvanikov 2019-05-08 17:08:06 +07:00
commit b6b5b16ba4
86 changed files with 2196 additions and 338 deletions

View file

@ -0,0 +1,25 @@
defmodule Mix.Tasks.Pleroma.Benchmark do
use Mix.Task
alias Mix.Tasks.Pleroma.Common
def run(["search"]) do
Common.start_pleroma()
Benchee.run(%{
"search" => fn ->
Pleroma.Web.MastodonAPI.MastodonAPIController.status_search(nil, "cofe")
end
})
end
def run(["tag"]) do
Common.start_pleroma()
Benchee.run(%{
"tag" => fn ->
%{"type" => "Create", "tag" => "cofe"}
|> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
end
})
end
end

View file

@ -126,7 +126,7 @@ defmodule Mix.Tasks.Pleroma.User do
proceed? = assume_yes? or Mix.shell().yes?("Continue?")
unless not proceed? do
if proceed? do
Common.start_pleroma()
params = %{
@ -163,7 +163,7 @@ defmodule Mix.Tasks.Pleroma.User do
Common.start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.delete(user)
User.perform(:delete, user)
Mix.shell().info("User #{nickname} deleted.")
else
_ ->
@ -380,7 +380,7 @@ defmodule Mix.Tasks.Pleroma.User do
Common.start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.delete_user_activities(user)
{:ok, _} = User.delete_user_activities(user)
Mix.shell().info("User #{nickname} statuses deleted.")
else
_ ->

View file

@ -6,14 +6,18 @@ defmodule Pleroma.Activity do
use Ecto.Schema
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
import Ecto.Changeset
import Ecto.Query
@type t :: %__MODULE__{}
@type actor :: String.t()
@primary_key {:id, Pleroma.FlakeId, autogenerate: true}
# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
@ -33,6 +37,8 @@ defmodule Pleroma.Activity do
field(:local, :boolean, default: true)
field(:actor, :string)
field(:recipients, {:array, :string}, default: [])
# This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark
has_one(:bookmark, Bookmark)
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!
@ -71,6 +77,16 @@ defmodule Pleroma.Activity do
|> preload([activity, object], object: object)
end
def with_preloaded_bookmark(query, %User{} = user) do
from([a] in query,
left_join: b in Bookmark,
on: b.user_id == ^user.id and b.activity_id == a.id,
preload: [bookmark: b]
)
end
def with_preloaded_bookmark(query, _), do: query
def get_by_ap_id(ap_id) do
Repo.one(
from(
@ -80,6 +96,16 @@ defmodule Pleroma.Activity do
)
end
def get_bookmark(%Activity{} = activity, %User{} = user) do
if Ecto.assoc_loaded?(activity.bookmark) do
activity.bookmark
else
Bookmark.get(user.id, activity.id)
end
end
def get_bookmark(_, _), do: nil
def change(struct, params \\ %{}) do
struct
|> cast(params, [:data])
@ -260,4 +286,9 @@ defmodule Pleroma.Activity do
|> where([s], s.actor == ^actor)
|> Repo.all()
end
@spec query_by_actor(actor()) :: Ecto.Query.t()
def query_by_actor(actor) do
from(a in Activity, where: a.actor == ^actor)
end
end

View file

@ -0,0 +1,16 @@
defmodule Pleroma.BBS.Authenticator do
use Sshd.PasswordAuthenticator
alias Comeonin.Pbkdf2
alias Pleroma.User
def authenticate(username, password) do
username = to_string(username)
password = to_string(password)
with %User{} = user <- User.get_by_nickname(username) do
Pbkdf2.checkpw(password, user.password_hash)
else
_e -> false
end
end
end

147
lib/pleroma/bbs/handler.ex Normal file
View file

@ -0,0 +1,147 @@
defmodule Pleroma.BBS.Handler do
use Sshd.ShellHandler
alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
def on_shell(username, _pubkey, _ip, _port) do
:ok = IO.puts("Welcome to #{Pleroma.Config.get([:instance, :name])}!")
user = Pleroma.User.get_cached_by_nickname(to_string(username))
Logger.debug("#{inspect(user)}")
loop(run_state(user: user))
end
def on_connect(username, ip, port, method) do
Logger.debug(fn ->
"""
Incoming SSH shell #{inspect(self())} requested for #{username} from #{inspect(ip)}:#{
inspect(port)
} using #{inspect(method)}
"""
end)
end
def on_disconnect(username, ip, port) do
Logger.debug(fn ->
"Disconnecting SSH shell for #{username} from #{inspect(ip)}:#{inspect(port)}"
end)
end
defp loop(state) do
self_pid = self()
counter = state.counter
prefix = state.prefix
user = state.user
input = spawn(fn -> io_get(self_pid, prefix, counter, user.nickname) end)
wait_input(state, input)
end
def puts_activity(activity) do
status = Pleroma.Web.MastodonAPI.StatusView.render("status.json", %{activity: activity})
IO.puts("-- #{status.id} by #{status.account.display_name} (#{status.account.acct})")
IO.puts(HtmlSanitizeEx.strip_tags(status.content))
IO.puts("")
end
def handle_command(state, "help") do
IO.puts("Available commands:")
IO.puts("help - This help")
IO.puts("home - Show the home timeline")
IO.puts("p <text> - Post the given text")
IO.puts("r <id> <text> - Reply to the post with the given id")
IO.puts("quit - Quit")
state
end
def handle_command(%{user: user} = state, "r " <> text) do
text = String.trim(text)
[activity_id, rest] = String.split(text, " ", parts: 2)
with %Activity{} <- Activity.get_by_id(activity_id),
{:ok, _activity} <-
CommonAPI.post(user, %{"status" => rest, "in_reply_to_status_id" => activity_id}) do
IO.puts("Replied!")
else
_e -> IO.puts("Could not reply...")
end
state
end
def handle_command(%{user: user} = state, "p " <> text) do
text = String.trim(text)
with {:ok, _activity} <- CommonAPI.post(user, %{"status" => text}) do
IO.puts("Posted!")
else
_e -> IO.puts("Could not post...")
end
state
end
def handle_command(state, "home") do
user = state.user
params =
%{}
|> Map.put("type", ["Create"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
activities =
[user.ap_id | user.following]
|> ActivityPub.fetch_activities(params)
|> ActivityPub.contain_timeline(user)
Enum.each(activities, fn activity ->
puts_activity(activity)
end)
state
end
def handle_command(state, command) do
IO.puts("Unknown command '#{command}'")
state
end
defp wait_input(state, input) do
receive do
{:input, ^input, "quit\n"} ->
IO.puts("Exiting...")
{:input, ^input, code} when is_binary(code) ->
code = String.trim(code)
state = handle_command(state, code)
loop(%{state | counter: state.counter + 1})
{:error, :interrupted} ->
IO.puts("Caught Ctrl+C...")
loop(%{state | counter: state.counter + 1})
{:input, ^input, msg} ->
:ok = Logger.warn("received unknown message: #{inspect(msg)}")
loop(%{state | counter: state.counter + 1})
end
end
defp run_state(opts) do
%{prefix: "pleroma", counter: 1, user: opts[:user]}
end
defp io_get(pid, prefix, counter, username) do
prompt = prompt(prefix, counter, username)
send(pid, {:input, self(), IO.gets(:stdio, prompt)})
end
defp prompt(prefix, counter, username) do
prompt = "#{username}@#{prefix}:#{counter}>"
prompt <> " "
end
end

View file

@ -0,0 +1,75 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Conversation do
alias Pleroma.Conversation.Participation
alias Pleroma.Repo
alias Pleroma.User
use Ecto.Schema
import Ecto.Changeset
schema "conversations" do
# This is the context ap id.
field(:ap_id, :string)
has_many(:participations, Participation)
has_many(:users, through: [:participations, :user])
timestamps()
end
def creation_cng(struct, params) do
struct
|> cast(params, [:ap_id])
|> validate_required([:ap_id])
|> unique_constraint(:ap_id)
end
def create_for_ap_id(ap_id) do
%__MODULE__{}
|> creation_cng(%{ap_id: ap_id})
|> Repo.insert(
on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]],
returning: true,
conflict_target: :ap_id
)
end
def get_for_ap_id(ap_id) do
Repo.get_by(__MODULE__, ap_id: ap_id)
end
@doc """
This will
1. Create a conversation if there isn't one already
2. Create a participation for all the people involved who don't have one already
3. Bump all relevant participations to 'unread'
"""
def create_or_bump_for(activity) do
with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity),
object <- Pleroma.Object.normalize(activity),
"Create" <- activity.data["type"],
"Note" <- object.data["type"],
ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do
{:ok, conversation} = create_for_ap_id(ap_id)
users = User.get_users_from_set(activity.recipients, false)
participations =
Enum.map(users, fn user ->
{:ok, participation} =
Participation.create_for_user_and_conversation(user, conversation)
participation
end)
{:ok,
%{
conversation
| participations: participations
}}
else
e -> {:error, e}
end
end
end

View file

@ -0,0 +1,81 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Conversation.Participation do
use Ecto.Schema
alias Pleroma.Conversation
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
import Ecto.Changeset
import Ecto.Query
schema "conversation_participations" do
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:conversation, Conversation)
field(:read, :boolean, default: false)
field(:last_activity_id, Pleroma.FlakeId, virtual: true)
timestamps()
end
def creation_cng(struct, params) do
struct
|> cast(params, [:user_id, :conversation_id])
|> validate_required([:user_id, :conversation_id])
end
def create_for_user_and_conversation(user, conversation) do
%__MODULE__{}
|> creation_cng(%{user_id: user.id, conversation_id: conversation.id})
|> Repo.insert(
on_conflict: [set: [read: false, updated_at: NaiveDateTime.utc_now()]],
returning: true,
conflict_target: [:user_id, :conversation_id]
)
end
def read_cng(struct, params) do
struct
|> cast(params, [:read])
|> validate_required([:read])
end
def mark_as_read(participation) do
participation
|> read_cng(%{read: true})
|> Repo.update()
end
def mark_as_unread(participation) do
participation
|> read_cng(%{read: false})
|> Repo.update()
end
def for_user(user, params \\ %{}) do
from(p in __MODULE__,
where: p.user_id == ^user.id,
order_by: [desc: p.updated_at]
)
|> Pleroma.Pagination.fetch_paginated(params)
|> Repo.preload(conversation: [:users])
end
def for_user_with_last_activity_id(user, params \\ %{}) do
for_user(user, params)
|> Enum.map(fn participation ->
activity_id =
ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{
"user" => user,
"blocking_user" => user
})
%{
participation
| last_activity_id: activity_id
}
end)
end
end

View file

@ -113,9 +113,7 @@ defmodule Pleroma.Formatter do
html =
if not strip do
"<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{
MediaProxy.url(file)
}' />"
"<img class='emoji' alt='#{emoji}' title='#{emoji}' src='#{MediaProxy.url(file)}' />"
else
""
end
@ -130,12 +128,23 @@ defmodule Pleroma.Formatter do
def demojify(text, nil), do: text
@doc "Outputs a list of the emoji-shortcodes in a text"
def get_emoji(text) when is_binary(text) do
Enum.filter(Emoji.get_all(), fn {emoji, _, _} -> String.contains?(text, ":#{emoji}:") end)
end
def get_emoji(_), do: []
@doc "Outputs a list of the emoji-Maps in a text"
def get_emoji_map(text) when is_binary(text) do
get_emoji(text)
|> Enum.reduce(%{}, fn {name, file, _group}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
end
def get_emoji_map(_), do: []
def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags}
end

View file

@ -28,12 +28,18 @@ 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_activity(content, scrubbers, activity, key \\ "") do
def get_cached_scrubbed_html_for_activity(
content,
scrubbers,
activity,
key \\ "",
callback \\ fn x -> x end
) 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)
ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback)
end)
end
@ -42,24 +48,27 @@ defmodule Pleroma.HTML do
content,
HtmlSanitizeEx.Scrubber.StripTags,
activity,
key
key,
&HtmlEntities.decode/1
)
end
def ensure_scrubbed_html(
content,
scrubbers,
false = _fake
fake,
callback
) do
{:commit, filter_tags(content, scrubbers)}
end
content =
content
|> filter_tags(scrubbers)
|> callback.()
def ensure_scrubbed_html(
content,
scrubbers,
true = _fake
) do
{:ignore, filter_tags(content, scrubbers)}
if fake do
{:ignore, content}
else
{:commit, content}
end
end
defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do
@ -142,6 +151,7 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
Meta.allow_tag_with_these_attributes("img", [
"width",
"height",
"class",
"title",
"alt"
])
@ -212,6 +222,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("img", [
"width",
"height",
"class",
"title",
"alt"
])

View file

@ -1,7 +1,5 @@
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.

View file

@ -35,7 +35,7 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
defp csp_string do
scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
static_url = Pleroma.Web.Endpoint.static_url()
websocket_url = String.replace(static_url, "http", "ws")
websocket_url = Pleroma.Web.Endpoint.websocket_url()
connect_src = "connect-src 'self' #{static_url} #{websocket_url}"

View file

@ -16,6 +16,16 @@ defmodule Pleroma.Plugs.OAuthPlug do
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(%{params: %{"access_token" => access_token}} = conn, _) do
with {:ok, user, token_record} <- fetch_user_and_token(access_token) do
conn
|> assign(:token, token_record)
|> assign(:user, user)
else
_ -> conn
end
end
def call(conn, _) do
with {:ok, token_str} <- fetch_token_str(conn),
{:ok, user, token_record} <- fetch_user_and_token(token_str) do

View file

@ -19,4 +19,32 @@ defmodule Pleroma.Repo do
def init(_, opts) do
{:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))}
end
@doc "find resource based on prepared query"
@spec find_resource(Ecto.Query.t()) :: {:ok, struct()} | {:error, :not_found}
def find_resource(%Ecto.Query{} = query) do
case __MODULE__.one(query) do
nil -> {:error, :not_found}
resource -> {:ok, resource}
end
end
def find_resource(_query), do: {:error, :not_found}
@doc """
Gets association from cache or loads if need
## Examples
iex> Repo.get_assoc(token, :user)
%User{}
"""
@spec get_assoc(struct(), atom()) :: {:ok, struct()} | {:error, :not_found}
def get_assoc(resource, association) do
case __MODULE__.preload(resource, association) do
%{^association => assoc} when not is_nil(assoc) -> {:ok, assoc}
_ -> {:error, :not_found}
end
end
end

View file

@ -4,7 +4,7 @@
defmodule Pleroma.Upload do
@moduledoc """
# Upload
Manage user uploads
Options:
* `:type`: presets for activity type (defaults to Document) and size limits from app configuration

View file

@ -10,8 +10,6 @@ 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
@ -56,7 +54,6 @@ defmodule Pleroma.User do
field(:tags, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime_usec)
field(:last_digest_emailed_at, :naive_datetime)
has_many(:bookmarks, Bookmark)
has_many(:notifications, Notification)
has_many(:registrations, Registration)
embeds_one(:info, Pleroma.User.Info)
@ -424,7 +421,7 @@ defmodule Pleroma.User do
Enum.map(
followed_identifiers,
fn followed_identifier ->
with %User{} = followed <- get_or_fetch(followed_identifier),
with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
{:ok, follower} <- maybe_direct_follow(follower, followed),
{:ok, _} <- ActivityPub.follow(follower, followed) do
followed
@ -508,7 +505,15 @@ defmodule Pleroma.User do
def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
Cachex.fetch!(:user_cache, key, fn ->
user_result = get_or_fetch_by_nickname(nickname)
case user_result do
{:ok, user} -> {:commit, user}
{:error, _error} -> {:ignore, nil}
end
end)
end
def get_cached_by_nickname_or_id(nickname_or_id) do
@ -544,7 +549,7 @@ defmodule Pleroma.User do
def get_or_fetch_by_nickname(nickname) do
with %User{} = user <- get_by_nickname(nickname) do
user
{:ok, user}
else
_e ->
with [_nick, _domain] <- String.split(nickname, "@"),
@ -554,9 +559,9 @@ defmodule Pleroma.User do
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
end
user
{:ok, user}
else
_e -> nil
_e -> {:error, "not found " <> nickname}
end
end
end
@ -903,7 +908,7 @@ defmodule Pleroma.User do
Enum.map(
blocked_identifiers,
fn blocked_identifier ->
with %User{} = blocked <- get_or_fetch(blocked_identifier),
with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
{:ok, blocker} <- block(blocker, blocked),
{:ok, _} <- ActivityPub.block(blocker, blocked) do
blocked
@ -1158,7 +1163,12 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
def delete(%User{} = user) do
@spec delete(User.t()) :: :ok
def delete(%User{} = user),
do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do
{:ok, user} = User.deactivate(user)
# Remove all relationships
@ -1174,22 +1184,23 @@ defmodule Pleroma.User do
end
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()
stream =
ap_id
|> Activity.query_by_actor()
|> Activity.with_preloaded_object()
|> Repo.stream()
# TODO: Do something with likes, follows, repeats.
_ ->
"Doing nothing"
end)
Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity)
{:ok, user}
end
defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
Object.normalize(activity) |> ActivityPub.delete()
end
defp delete_activity(_activity), do: "Doing nothing"
def html_filter_policy(%User{info: %{no_rich_text: true}}) do
Pleroma.HTML.Scrubber.TwitterText
end
@ -1203,11 +1214,11 @@ defmodule Pleroma.User do
case ap_try do
{:ok, user} ->
user
{:ok, user}
_ ->
case OStatus.make_user(ap_id) do
{:ok, user} -> user
{:ok, user} -> {:ok, user}
_ -> {:error, "Could not fetch by AP id"}
end
end
@ -1217,20 +1228,20 @@ defmodule Pleroma.User do
user = get_cached_by_ap_id(ap_id)
if !is_nil(user) and !User.needs_update?(user) do
user
{:ok, user}
else
# 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])
user = fetch_by_ap_id(ap_id)
resp = fetch_by_ap_id(ap_id)
if should_fetch_initial do
with %User{} = user do
with {:ok, %User{} = user} = resp do
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
end
end
user
resp
end
end
@ -1272,7 +1283,7 @@ defmodule Pleroma.User do
end
def get_public_key_for_ap_id(ap_id) do
with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
{:ok, public_key} <- public_key_from_info(user.info) do
{:ok, public_key}
else
@ -1324,18 +1335,15 @@ defmodule Pleroma.User do
end
end
def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
def parse_bio(nil, _user), do: ""
def parse_bio(bio, _user) when bio == "", do: bio
def parse_bio(bio) when is_binary(bio) and bio != "" do
bio
|> CommonUtils.format_input("text/plain", mentions_format: :full)
|> elem(0)
end
def parse_bio(bio, user) do
emoji =
(user.info.source_data["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
{String.trim(name, ":"), url}
end)
def parse_bio(_), do: ""
def parse_bio(bio, user) when is_binary(bio) and bio != "" do
# TODO: get profile URLs other than user.ap_id
profile_urls = [user.ap_id]
@ -1345,9 +1353,10 @@ defmodule Pleroma.User do
rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
)
|> elem(0)
|> Formatter.emojify(emoji)
end
def parse_bio(_, _), do: ""
def tag(user_identifiers, tags) when is_list(user_identifiers) do
Repo.transaction(fn ->
for user_identifier <- user_identifiers, do: tag(user_identifier, tags)

View file

@ -44,6 +44,7 @@ defmodule Pleroma.User.Info do
field(:pinned_activities, {:array, :string}, default: [])
field(:flavour, :string, default: nil)
field(:email_notifications, :map, default: %{"digest" => false})
field(:emoji, {:array, :map}, default: [])
field(:notification_settings, :map,
default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true}

View file

@ -24,7 +24,7 @@ defmodule Pleroma.UserInviteToken do
timestamps()
end
@spec create_invite(map()) :: UserInviteToken.t()
@spec create_invite(map()) :: {:ok, UserInviteToken.t()}
def create_invite(params \\ %{}) do
%UserInviteToken{}
|> cast(params, [:max_use, :expires_at])

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity
alias Pleroma.Conversation
alias Pleroma.Instances
alias Pleroma.Notification
alias Pleroma.Object
@ -141,7 +142,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end)
Notification.create_notifications(activity)
participations =
activity
|> Conversation.create_or_bump_for()
|> get_participations()
stream_out(activity)
stream_out_participations(participations)
{:ok, activity}
else
%Activity{} = activity ->
@ -164,11 +172,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
defp get_participations({:ok, %{participations: participations}}), do: participations
defp get_participations(_), do: []
def stream_out_participations(participations) do
participations =
participations
|> Repo.preload(:user)
Enum.each(participations, fn participation ->
Pleroma.Web.Streamer.stream("participation", participation)
end)
end
def stream_out(activity) do
public = "https://www.w3.org/ns/activitystreams#Public"
if activity.data["type"] in ["Create", "Announce", "Delete"] do
object = Object.normalize(activity)
Pleroma.Web.Streamer.stream("user", activity)
Pleroma.Web.Streamer.stream("list", activity)
@ -180,6 +200,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
if activity.data["type"] in ["Create"] do
object = Object.normalize(activity)
object.data
|> Map.get("tag", [])
|> Enum.filter(fn tag -> is_bitstring(tag) end)
@ -194,6 +216,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
else
# TODO: Write test, replace with visibility test
if !Enum.member?(activity.data["cc"] || [], public) &&
!Enum.member?(
activity.data["to"],
@ -456,35 +479,44 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
def fetch_activities_for_context(context, opts \\ %{}) do
defp fetch_activities_for_context_query(context, opts) do
public = ["https://www.w3.org/ns/activitystreams#Public"]
recipients =
if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public
query = from(activity in Activity)
query =
query
|> restrict_blocked(opts)
|> restrict_recipients(recipients, opts["user"])
query =
from(
activity in query,
where:
fragment(
"?->>'type' = ? and ?->>'context' = ?",
activity.data,
"Create",
activity.data,
^context
),
order_by: [desc: :id]
from(activity in Activity)
|> restrict_blocked(opts)
|> restrict_recipients(recipients, opts["user"])
|> where(
[activity],
fragment(
"?->>'type' = ? and ?->>'context' = ?",
activity.data,
"Create",
activity.data,
^context
)
|> Activity.with_preloaded_object()
)
|> order_by([activity], desc: activity.id)
end
Repo.all(query)
@spec fetch_activities_for_context(String.t(), keyword() | map()) :: [Activity.t()]
def fetch_activities_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(opts)
|> Activity.with_preloaded_object()
|> Repo.all()
end
@spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) ::
Pleroma.FlakeId.t() | nil
def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(opts)
|> limit(1)
|> select([a], a.id)
|> Repo.one()
end
def fetch_public_activities(opts \\ %{}) do
@ -783,11 +815,32 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Activity.with_preloaded_object()
end
defp maybe_preload_bookmarks(query, %{"skip_preload" => true}), do: query
defp maybe_preload_bookmarks(query, opts) do
query
|> Activity.with_preloaded_bookmark(opts["user"])
end
defp maybe_order(query, %{order: :desc}) do
query
|> order_by(desc: :id)
end
defp maybe_order(query, %{order: :asc}) do
query
|> order_by(asc: :id)
end
defp maybe_order(query, _), do: query
def fetch_activities_query(recipients, opts \\ %{}) do
base_query = from(activity in Activity)
base_query
|> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts)
|> maybe_order(opts)
|> restrict_recipients(recipients, opts["user"])
|> restrict_tag(opts)
|> restrict_tag_reject(opts)

View file

@ -155,7 +155,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
with %User{} = recipient <- User.get_cached_by_nickname(nickname),
%User{} = actor <- User.get_or_fetch_by_ap_id(params["actor"]),
{:ok, %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)

View file

@ -5,6 +5,8 @@
defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
alias Pleroma.User
@moduledoc "Prevent followbots from following with a bit of heuristic"
@behaviour Pleroma.Web.ActivityPub.MRF
# XXX: this should become User.normalize_by_ap_id() or similar, really.

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do
require Logger
@moduledoc "Drop and log everything received"
@behaviour Pleroma.Web.ActivityPub.MRF
@impl true

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
alias Pleroma.Object
@moduledoc "Ensure a re: is prepended on replies to a post with a Subject"
@behaviour Pleroma.Web.ActivityPub.MRF
@reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless])

View file

@ -4,6 +4,8 @@
defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
alias Pleroma.User
@moduledoc "Block messages with too much mentions (configurable)"
@behaviour Pleroma.Web.ActivityPub.MRF
defp delist_message(message, threshold) when threshold > 0 do

View file

@ -3,6 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
@moduledoc "Reject or Word-Replace messages with a keyword or regex"
@behaviour Pleroma.Web.ActivityPub.MRF
defp string_matches?(string, _) when not is_binary(string) do
false

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do
@moduledoc "Ensure no content placeholder is present (such as the dot from mastodon)"
@behaviour Pleroma.Web.ActivityPub.MRF
@impl true

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.NoOpPolicy do
@moduledoc "Does nothing (lets the messages go through unmodified)"
@behaviour Pleroma.Web.ActivityPub.MRF
@impl true

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
@moduledoc "Scrub configured hypertext markup"
alias Pleroma.HTML
@behaviour Pleroma.Web.ActivityPub.MRF

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
alias Pleroma.User
@moduledoc "Rejects non-public (followers-only, direct) activities"
@behaviour Pleroma.Web.ActivityPub.MRF
@impl true

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
alias Pleroma.User
@moduledoc "Filter activities depending on their origin instance"
@behaviour Pleroma.Web.ActivityPub.MRF
defp check_accept(%{host: actor_host} = _actor_info, object) do

View file

@ -5,6 +5,19 @@
defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF
@moduledoc """
Apply policies based on user tags
This policy applies policies on a user activities depending on their tags
on your instance.
- `mrf_tag:media-force-nsfw`: Mark as sensitive on presence of attachments
- `mrf_tag:media-strip`: Remove attachments
- `mrf_tag:force-unlisted`: Mark as unlisted (removes from the federated timeline)
- `mrf_tag:sandbox`: Remove from public (local and federated) timelines
- `mrf_tag:disable-remote-subscription`: Reject non-local follow requests
- `mrf_tag:disable-any-subscription`: Reject any follow requests
"""
defp get_tags(%User{tags: tags}) when is_list(tags), do: tags
defp get_tags(_), do: []

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
alias Pleroma.Config
@moduledoc "Accept-list of users from specified instances"
@behaviour Pleroma.Web.ActivityPub.MRF
defp filter_by_list(object, []), do: {:ok, object}

View file

@ -15,7 +15,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do
def follow(target_instance) do
with %User{} = local_user <- get_actor(),
%User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, activity} <- ActivityPub.follow(local_user, target_user) do
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity}
@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do
def unfollow(target_instance) do
with %User{} = local_user <- get_actor(),
%User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do
Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity}

View file

@ -126,7 +126,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_implicit_addressing(object, _), do: object
def fix_addressing(object) do
%User{} = user = User.get_or_fetch_by_ap_id(object["actor"])
{:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
followers_collection = User.ap_followers(user)
object
@ -407,7 +407,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> fix_addressing
with nil <- Activity.get_create_by_object_ap_id(object["id"]),
%User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
{:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
object = fix_object(data["object"])
params = %{
@ -436,7 +436,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
%User{} = follower <- User.get_or_fetch_by_ap_id(follower),
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
{:user_blocked, false} <-
@ -485,7 +485,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
) do
with actor <- Containment.get_actor(data),
%User{} = followed <- User.get_or_fetch_by_ap_id(actor),
{: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"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
@ -511,7 +511,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
) do
with actor <- Containment.get_actor(data),
%User{} = followed <- User.get_or_fetch_by_ap_id(actor),
{: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"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
@ -535,7 +535,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
%{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
) do
with actor <- Containment.get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
{:ok, activity}
@ -548,7 +548,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
) do
with actor <- Containment.get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
public <- Visibility.is_public?(data),
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
@ -603,7 +603,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
object_id = Utils.get_ap_id(object_id)
with actor <- Containment.get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
:ok <- Containment.contain_origin(actor.ap_id, object.data),
{:ok, activity} <- ActivityPub.delete(object, false) do
@ -622,7 +622,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
} = data
) do
with actor <- Containment.get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
{:ok, activity}
@ -640,7 +640,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
} = _data
) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
%User{} = follower <- User.get_or_fetch_by_ap_id(follower),
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
User.unfollow(follower, followed)
{:ok, activity}
@ -659,7 +659,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
) do
with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
%User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
%User{} = blocker <- User.get_or_fetch_by_ap_id(blocker),
{:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
User.unblock(blocker, blocked)
{:ok, activity}
@ -673,7 +673,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
) do
with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
%User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
%User{} = blocker = User.get_or_fetch_by_ap_id(blocker),
{:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
User.unfollow(blocker, blocked)
User.block(blocker, blocked)
@ -692,7 +692,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
} = data
) do
with actor <- Containment.get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
{:ok, activity}
@ -856,10 +856,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("tag", tags ++ mentions)
end
def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
user_info = add_emoji_tags(user_info)
object
|> Map.put(:info, user_info)
end
# TODO: we should probably send mtime instead of unix epoch time for updated
def add_emoji_tags(object) do
def add_emoji_tags(%{"emoji" => emoji} = object) do
tags = object["tag"] || []
emoji = object["emoji"] || []
out =
emoji
@ -877,6 +883,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("tag", tags ++ out)
end
def add_emoji_tags(object) do
object
end
def set_conversation(object) do
Map.put(object, "conversation", object["context"])
end

View file

@ -69,6 +69,11 @@ defmodule Pleroma.Web.ActivityPub.UserView do
endpoints = render("endpoints.json", %{user: user})
user_tags =
user
|> Transmogrifier.add_emoji_tags()
|> Map.get("tag", [])
%{
"id" => user.ap_id,
"type" => "Person",
@ -87,7 +92,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"publicKeyPem" => public_key
},
"endpoints" => endpoints,
"tag" => user.info.source_data["tag"] || []
"tag" => (user.info.source_data["tag"] || []) ++ user_tags
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))

View file

@ -42,4 +42,30 @@ defmodule Pleroma.Web.Auth.Authenticator do
implementation().oauth_consumer_template() ||
Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html")
end
@doc "Gets user by nickname or email for auth."
@spec fetch_user(String.t()) :: User.t() | nil
def fetch_user(name) do
User.get_by_nickname_or_email(name)
end
# Gets name and password from conn
#
@spec fetch_credentials(Plug.Conn.t() | map()) ::
{:ok, {name :: any, password :: any}} | {:error, :invalid_credentials}
def fetch_credentials(%Plug.Conn{params: params} = _),
do: fetch_credentials(params)
def fetch_credentials(params) do
case params do
%{"authorization" => %{"name" => name, "password" => password}} ->
{:ok, {name, password}}
%{"grant_type" => "password", "username" => name, "password" => password} ->
{:ok, {name, password}}
_ ->
{:error, :invalid_credentials}
end
end
end

View file

@ -7,6 +7,9 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
require Logger
import Pleroma.Web.Auth.Authenticator,
only: [fetch_credentials: 1, fetch_user: 1]
@behaviour Pleroma.Web.Auth.Authenticator
@base Pleroma.Web.Auth.PleromaAuthenticator
@ -20,30 +23,20 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
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
with {:ldap, true} <- {:ldap, Pleroma.Config.get([:ldap, :enabled])},
{:ok, {name, password}} <- fetch_credentials(conn),
%User{} = user <- ldap_user(name, password) do
{:ok, user}
else
# Fall back to default authenticator
@base.get_user(conn)
{:error, {:ldap_connection_error, _}} ->
# When LDAP is unavailable, try default authenticator
@base.get_user(conn)
{:ldap, _} ->
@base.get_user(conn)
error ->
error
end
end
@ -94,7 +87,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
case :eldap.simple_bind(connection, "#{uid}=#{name},#{base}", password) do
:ok ->
case User.get_by_nickname_or_email(name) do
case fetch_user(name) do
%User{} = user ->
user

View file

@ -8,19 +8,14 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
alias Pleroma.Repo
alias Pleroma.User
import Pleroma.Web.Auth.Authenticator,
only: [fetch_credentials: 1, fetch_user: 1]
@behaviour Pleroma.Web.Auth.Authenticator
def get_user(%Plug.Conn{} = conn) do
{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)},
with {:ok, {name, password}} <- fetch_credentials(conn),
{_, %User{} = user} <- {:user, fetch_user(name)},
{_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do
{:ok, user}
else

View file

@ -151,8 +151,8 @@ defmodule Pleroma.Web.CommonAPI do
),
{to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility),
context <- make_context(in_reply_to),
cw <- data["spoiler_text"],
full_payload <- String.trim(status <> (data["spoiler_text"] || "")),
cw <- data["spoiler_text"] || "",
full_payload <- String.trim(status <> cw),
length when length in 1..limit <- String.length(full_payload),
object <-
make_note_data(
@ -170,10 +170,7 @@ defmodule Pleroma.Web.CommonAPI do
Map.put(
object,
"emoji",
(Formatter.get_emoji(status) ++ Formatter.get_emoji(data["spoiler_text"]))
|> Enum.reduce(%{}, fn {name, file, _}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
Formatter.get_emoji_map(full_payload)
) do
res =
ActivityPub.create(

View file

@ -8,7 +8,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Filter
alias Pleroma.Formatter
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Fetcher
@ -23,6 +25,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.AppView
alias Pleroma.Web.MastodonAPI.ConversationView
alias Pleroma.Web.MastodonAPI.FilterView
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI
@ -86,7 +89,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
user_params =
%{}
|> add_if_present(params, "display_name", :name)
|> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
|> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
|> add_if_present(params, "avatar", :avatar, fn value ->
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :avatar) do
@ -96,6 +99,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end)
emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
user_info_emojis =
((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
|> Enum.dedup()
info_params =
[:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
|> Enum.reduce(%{}, fn key, acc ->
@ -112,6 +121,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
_ -> :error
end
end)
|> Map.put(:emoji, user_info_emojis)
info_cng = User.Info.profile_update(user.info, info_params)
@ -157,7 +167,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
@mastodon_api_level "2.5.0"
@mastodon_api_level "2.6.5"
def masto_instance(conn, _params) do
instance = Config.get(:instance)
@ -285,8 +295,6 @@ 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)
@ -305,8 +313,6 @@ 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)
@ -314,8 +320,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- User.get_cached_by_id(params["id"]),
reading_user <- Repo.preload(reading_user, :bookmarks) do
with %User{} = user <- User.get_cached_by_id(params["id"]) do
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
conn
@ -342,8 +347,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> ActivityPub.fetch_activities_query(params)
|> Pagination.fetch_paginated(params)
user = Repo.preload(user, bookmarks: :activity)
conn
|> add_link_headers(:dm_timeline, activities)
|> put_view(StatusView)
@ -353,8 +356,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
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})
@ -504,8 +505,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) 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})
@ -515,8 +514,6 @@ 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_with_object(id) do
user = Repo.preload(user, bookmarks: :activity)
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
@ -567,8 +564,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
{: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})
@ -580,8 +575,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
{: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})
@ -704,7 +697,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
def favourited_by(conn, %{"id" => id}) do
def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => 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)
@ -712,13 +705,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
conn
|> put_view(AccountView)
|> render(AccountView, "accounts.json", %{users: users, as: :user})
|> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
else
_ -> json(conn, [])
end
end
def reblogged_by(conn, %{"id" => id}) do
def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => 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)
@ -726,7 +719,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
conn
|> put_view(AccountView)
|> render("accounts.json", %{users: users, as: :user})
|> render("accounts.json", %{for: user, users: users, as: :user})
else
_ -> json(conn, [])
end
@ -783,7 +776,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
conn
|> add_link_headers(:followers, followers, user)
|> put_view(AccountView)
|> render("accounts.json", %{users: followers, as: :user})
|> render("accounts.json", %{for: for_user, users: followers, as: :user})
end
end
@ -800,7 +793,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
conn
|> add_link_headers(:following, followers, user)
|> put_view(AccountView)
|> render("accounts.json", %{users: followers, as: :user})
|> render("accounts.json", %{for: for_user, users: followers, as: :user})
end
end
@ -808,7 +801,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
with {:ok, follow_requests} <- User.get_follow_requests(followed) do
conn
|> put_view(AccountView)
|> render("accounts.json", %{users: follow_requests, as: :user})
|> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
end
end
@ -1102,8 +1095,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
ActivityPub.fetch_activities([], params)
|> Enum.reverse()
user = Repo.preload(user, bookmarks: :activity)
conn
|> add_link_headers(:favourites, activities)
|> put_view(StatusView)
@ -1149,7 +1140,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
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)
@ -1157,7 +1147,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
activities =
bookmarks
|> Enum.map(fn b -> b.activity end)
|> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
conn
|> add_link_headers(:bookmarks, bookmarks)
@ -1235,7 +1225,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
{:ok, users} = Pleroma.List.get_following(list) do
conn
|> put_view(AccountView)
|> render("accounts.json", %{users: users, as: :user})
|> render("accounts.json", %{for: user, users: users, as: :user})
end
end
@ -1266,8 +1256,6 @@ 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})
@ -1295,8 +1283,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
initial_state =
%{
meta: %{
streaming_api_base_url:
String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
access_token: token,
locale: "en",
domain: Pleroma.Web.Endpoint.host(),
@ -1653,7 +1640,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
x,
"id",
case User.get_or_fetch(x["acct"]) do
%{id: id} -> id
{:ok, %User{id: id}} -> id
_ -> 0
end
)
@ -1705,6 +1692,31 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
def conversations(%{assigns: %{user: user}} = conn, params) do
participations = Participation.for_user_with_last_activity_id(user, params)
conversations =
Enum.map(participations, fn participation ->
ConversationView.render("participation.json", %{participation: participation, user: user})
end)
conn
|> add_link_headers(:conversations, participations)
|> json(conversations)
end
def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
with %Participation{} = participation <-
Repo.get_by(Participation, id: participation_id, user_id: user.id),
{:ok, participation} <- Participation.mark_as_read(participation) do
participation_view =
ConversationView.render("participation.json", %{participation: participation, user: user})
conn
|> json(participation_view)
end
end
def try_render(conn, target, params)
when is_binary(target) do
res = render(conn, target, params)

View file

@ -0,0 +1,38 @@
defmodule Pleroma.Web.MastodonAPI.ConversationView do
use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
def render("participation.json", %{participation: participation, user: user}) do
participation = Repo.preload(participation, conversation: :users)
last_activity_id =
with nil <- participation.last_activity_id do
ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{
"user" => user,
"blocking_user" => user
})
end
activity = Activity.get_by_id_with_object(last_activity_id)
last_status = StatusView.render("status.json", %{activity: activity, for: user})
accounts =
AccountView.render("accounts.json", %{
users: participation.conversation.users,
as: :user
})
%{
id: participation.id |> to_string(),
accounts: accounts,
unread: !participation.read,
last_status: last_status
}
end
end

View file

@ -75,18 +75,22 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
def render(
"status.json",
%{activity: %{data: %{"type" => "Announce", "object" => object}} = activity} = opts
%{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
) do
user = get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])
activity_object = Object.normalize(activity)
reblogged_activity =
Activity.create_by_object_ap_id(activity_object.data["id"])
|> Activity.with_preloaded_bookmark(opts[:for])
|> Repo.one()
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)
bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil
mentions =
activity.recipients
@ -96,8 +100,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
%{
id: to_string(activity.id),
uri: object,
url: object,
uri: activity_object.data["id"],
url: activity_object.data["id"],
account: AccountView.render("account.json", %{user: user}),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
@ -149,7 +153,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
bookmarked = opts[:for] && CommonAPI.bookmarked?(opts[:for], activity)
bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
attachment_data = object.data["attachment"] || []
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.App do
use Ecto.Schema
import Ecto.Changeset
@type t :: %__MODULE__{}
schema "apps" do
field(:client_name, :string)
field(:redirect_uris, :string)

View file

@ -13,6 +13,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
import Ecto.Changeset
import Ecto.Query
@type t :: %__MODULE__{}
schema "oauth_authorizations" do
field(:token, :string)
field(:scopes, {:array, :string}, default: [])
@ -63,4 +64,11 @@ defmodule Pleroma.Web.OAuth.Authorization do
)
|> Repo.delete_all()
end
@doc "gets auth for app by token"
@spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_token(%App{id: app_id} = _app, token) do
from(t in __MODULE__, where: t.app_id == ^app_id and t.token == ^token)
|> Repo.find_resource()
end
end

View file

@ -13,11 +13,15 @@ defmodule Pleroma.Web.OAuth.OAuthController do
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
@expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600)
plug(:fetch_session)
plug(:fetch_flash)
@ -138,25 +142,33 @@ defmodule Pleroma.Web.OAuth.OAuthController do
Authenticator.handle_error(conn, error)
end
@doc "Renew access_token with refresh_token"
def token_exchange(
conn,
%{"grant_type" => "refresh_token", "refresh_token" => token} = params
) do
with %App{} = app <- get_app_from_request(conn, params),
{:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
{:ok, token} <- RefreshToken.grant(token) do
response_attrs = %{created_at: Token.Utils.format_created_at(token)}
json(conn, response_token(user, token, response_attrs))
else
_error ->
put_status(conn, 400)
|> json(%{error: "Invalid credentials"})
end
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),
fixed_token = Token.Utils.fix_padding(params["code"]),
{:ok, auth} <- Authorization.get_by_token(app, fixed_token),
%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 = %{
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
created_at: DateTime.to_unix(inserted_at),
expires_in: 60 * 10,
scope: Enum.join(token.scopes, " "),
me: user.ap_id
}
{:ok, token} <- Token.exchange_token(app, auth) do
response_attrs = %{created_at: Token.Utils.format_created_at(token)}
json(conn, response)
json(conn, response_token(user, token, response_attrs))
else
_error ->
put_status(conn, 400)
@ -177,16 +189,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
true <- Enum.any?(scopes),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:ok, token} <- Token.exchange_token(app, auth) do
response = %{
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
expires_in: 60 * 10,
scope: Enum.join(token.scopes, " "),
me: user.ap_id
}
json(conn, response)
json(conn, response_token(user, token))
else
{:auth_active, false} ->
# Per https://github.com/tootsuite/mastodon/blob/
@ -218,10 +221,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do
token_exchange(conn, params)
end
def token_revoke(conn, %{"token" => token} = params) do
# Bad request
def token_exchange(conn, params), do: bad_request(conn, params)
def token_revoke(conn, %{"token" => _token} = params) do
with %App{} = app <- get_app_from_request(conn, params),
%Token{} = token <- Repo.get_by(Token, token: token, app_id: app.id),
{:ok, %Token{}} <- Repo.delete(token) do
{:ok, _token} <- RevokeToken.revoke(app, params) do
json(conn, %{})
else
_error ->
@ -230,6 +235,15 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end
def token_revoke(conn, params), do: bad_request(conn, params)
# Response for bad request
defp bad_request(conn, _) do
conn
|> put_status(500)
|> json(%{error: "Bad request"})
end
@doc "Prepares OAuth request to provider for Ueberauth"
def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attrs}) do
scope =
@ -278,25 +292,22 @@ defmodule Pleroma.Web.OAuth.OAuthController 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)
})
case Repo.get_assoc(registration, :user) do
{:ok, user} ->
create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
conn
|> put_session(:registration_id, registration.id)
|> registration_details(%{"authorization" => registration_params})
_ ->
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
_ ->
@ -399,36 +410,30 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end
# XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be
# decoding it. Investigate sometime.
defp fix_padding(token) do
token
|> URI.decode()
|> Base.url_decode64!(padding: false)
|> Base.url_encode64(padding: false)
defp get_app_from_request(conn, params) do
conn
|> fetch_client_credentials(params)
|> fetch_client
end
defp get_app_from_request(conn, params) do
# Per RFC 6749, HTTP Basic is preferred to body params
{client_id, client_secret} =
with ["Basic " <> encoded] <- get_req_header(conn, "authorization"),
{:ok, decoded} <- Base.decode64(encoded),
[id, secret] <-
String.split(decoded, ":")
|> Enum.map(fn s -> URI.decode_www_form(s) end) do
{id, secret}
else
_ -> {params["client_id"], params["client_secret"]}
end
defp fetch_client({id, secret}) when is_binary(id) and is_binary(secret) do
Repo.get_by(App, client_id: id, client_secret: secret)
end
if client_id && client_secret do
Repo.get_by(
App,
client_id: client_id,
client_secret: client_secret
)
defp fetch_client({_id, _secret}), do: nil
defp fetch_client_credentials(conn, params) do
# Per RFC 6749, HTTP Basic is preferred to body params
with ["Basic " <> encoded] <- get_req_header(conn, "authorization"),
{:ok, decoded} <- Base.decode64(encoded),
[id, secret] <-
Enum.map(
String.split(decoded, ":"),
fn s -> URI.decode_www_form(s) end
) do
{id, secret}
else
nil
_ -> {params["client_id"], params["client_secret"]}
end
end
@ -441,4 +446,16 @@ defmodule Pleroma.Web.OAuth.OAuthController do
defp put_session_registration_id(conn, registration_id),
do: put_session(conn, :registration_id, registration_id)
defp response_token(%User{} = user, token, opts \\ %{}) do
%{
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
expires_in: @expires_in,
scope: Enum.join(token.scopes, " "),
me: user.ap_id
}
|> Map.merge(opts)
end
end

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.Token do
use Ecto.Schema
import Ecto.Query
import Ecto.Changeset
alias Pleroma.Repo
alias Pleroma.User
@ -13,6 +14,9 @@ defmodule Pleroma.Web.OAuth.Token do
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
@expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600)
@type t :: %__MODULE__{}
schema "oauth_tokens" do
field(:token, :string)
field(:refresh_token, :string)
@ -24,28 +28,67 @@ defmodule Pleroma.Web.OAuth.Token do
timestamps()
end
@doc "Gets token for app by access token"
@spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_token(%App{id: app_id} = _app, token) do
from(t in __MODULE__, where: t.app_id == ^app_id and t.token == ^token)
|> Repo.find_resource()
end
@doc "Gets token for app by refresh token"
@spec get_by_refresh_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_refresh_token(%App{id: app_id} = _app, token) do
from(t in __MODULE__,
where: t.app_id == ^app_id and t.refresh_token == ^token,
preload: [:user]
)
|> Repo.find_resource()
end
def exchange_token(app, auth) do
with {:ok, auth} <- Authorization.use_token(auth),
true <- auth.app_id == app.id do
create_token(app, User.get_cached_by_id(auth.user_id), auth.scopes)
create_token(
app,
User.get_cached_by_id(auth.user_id),
%{scopes: auth.scopes}
)
end
end
def create_token(%App{} = app, %User{} = user, scopes \\ nil) do
scopes = scopes || app.scopes
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
refresh_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
defp put_token(changeset) do
changeset
|> change(%{token: Token.Utils.generate_token()})
|> validate_required([:token])
|> unique_constraint(:token)
end
token = %Token{
token: token,
refresh_token: refresh_token,
scopes: scopes,
user_id: user.id,
app_id: app.id,
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
}
defp put_refresh_token(changeset, attrs) do
refresh_token = Map.get(attrs, :refresh_token, Token.Utils.generate_token())
Repo.insert(token)
changeset
|> change(%{refresh_token: refresh_token})
|> validate_required([:refresh_token])
|> unique_constraint(:refresh_token)
end
defp put_valid_until(changeset, attrs) do
expires_in =
Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), @expires_in))
changeset
|> change(%{valid_until: expires_in})
|> validate_required([:valid_until])
end
def create_token(%App{} = app, %User{} = user, attrs \\ %{}) do
%__MODULE__{user_id: user.id, app_id: app.id}
|> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes])
|> validate_required([:scopes, :user_id, :app_id])
|> put_valid_until(attrs)
|> put_token
|> put_refresh_token(attrs)
|> Repo.insert()
end
def delete_user_tokens(%User{id: user_id}) do
@ -73,4 +116,10 @@ defmodule Pleroma.Web.OAuth.Token do
|> Repo.all()
|> Repo.preload(:app)
end
def is_expired?(%__MODULE__{valid_until: valid_until}) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0
end
def is_expired?(_), do: false
end

View file

@ -0,0 +1,54 @@
defmodule Pleroma.Web.OAuth.Token.Strategy.RefreshToken do
@moduledoc """
Functions for dealing with refresh token strategy.
"""
alias Pleroma.Config
alias Pleroma.Repo
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Strategy.Revoke
@doc """
Will grant access token by refresh token.
"""
@spec grant(Token.t()) :: {:ok, Token.t()} | {:error, any()}
def grant(token) do
access_token = Repo.preload(token, [:user, :app])
result =
Repo.transaction(fn ->
token_params = %{
app: access_token.app,
user: access_token.user,
scopes: access_token.scopes
}
access_token
|> revoke_access_token()
|> create_access_token(token_params)
end)
case result do
{:ok, {:error, reason}} -> {:error, reason}
{:ok, {:ok, token}} -> {:ok, token}
{:error, reason} -> {:error, reason}
end
end
defp revoke_access_token(token) do
Revoke.revoke(token)
end
defp create_access_token({:error, error}, _), do: {:error, error}
defp create_access_token({:ok, token}, %{app: app, user: user} = token_params) do
Token.create_token(app, user, add_refresh_token(token_params, token.refresh_token))
end
defp add_refresh_token(params, token) do
case Config.get([:oauth2, :issue_new_refresh_token], false) do
true -> Map.put(params, :refresh_token, token)
false -> params
end
end
end

View file

@ -0,0 +1,22 @@
defmodule Pleroma.Web.OAuth.Token.Strategy.Revoke do
@moduledoc """
Functions for dealing with revocation.
"""
alias Pleroma.Repo
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Token
@doc "Finds and revokes access token for app and by token"
@spec revoke(App.t(), map()) :: {:ok, Token.t()} | {:error, :not_found | Ecto.Changeset.t()}
def revoke(%App{} = app, %{"token" => token} = _attrs) do
with {:ok, token} <- Token.get_by_token(app, token),
do: revoke(token)
end
@doc "Revokes access token"
@spec revoke(Token.t()) :: {:ok, Token.t()} | {:error, Ecto.Changeset.t()}
def revoke(%Token{} = token) do
Repo.delete(token)
end
end

View file

@ -0,0 +1,30 @@
defmodule Pleroma.Web.OAuth.Token.Utils do
@moduledoc """
Auxiliary functions for dealing with tokens.
"""
@doc "convert token inserted_at to unix timestamp"
def format_created_at(%{inserted_at: inserted_at} = _token) do
inserted_at
|> DateTime.from_naive!("Etc/UTC")
|> DateTime.to_unix()
end
@doc false
@spec generate_token(keyword()) :: binary()
def generate_token(opts \\ []) do
opts
|> Keyword.get(:size, 32)
|> :crypto.strong_rand_bytes()
|> Base.url_encode64(padding: false)
end
# XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be
# decoding it. Investigate sometime.
def fix_padding(token) do
token
|> URI.decode()
|> Base.url_decode64!(padding: false)
|> Base.url_encode64(padding: false)
end
end

View file

@ -276,6 +276,9 @@ defmodule Pleroma.Web.Router do
get("/suggestions", MastodonAPIController, :suggestions)
get("/conversations", MastodonAPIController, :conversations)
post("/conversations/:id/read", MastodonAPIController, :conversation_read)
get("/endorsements", MastodonAPIController, :empty_array)
get("/pleroma/flavour", MastodonAPIController, :get_flavour)

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.Streamer do
use GenServer
require Logger
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.User
@ -71,6 +72,15 @@ defmodule Pleroma.Web.Streamer do
{:noreply, topics}
end
def handle_cast(%{action: :stream, topic: "participation", item: participation}, topics) do
user_topic = "direct:#{participation.user_id}"
Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n")
push_to_socket(topics, user_topic, participation)
{:noreply, topics}
end
def handle_cast(%{action: :stream, topic: "list", item: item}, topics) do
# filter the recipient list if the activity is not public, see #270.
recipient_lists =
@ -192,6 +202,19 @@ defmodule Pleroma.Web.Streamer do
|> Jason.encode!()
end
def represent_conversation(%Participation{} = participation) do
%{
event: "conversation",
payload:
Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{
participation: participation,
user: participation.user
})
|> Jason.encode!()
}
|> Jason.encode!()
end
def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = item) do
Enum.each(topics[topic] || [], fn socket ->
# Get the current user so we have up-to-date blocks etc.
@ -214,6 +237,12 @@ defmodule Pleroma.Web.Streamer do
end)
end
def push_to_socket(topics, topic, %Participation{} = participation) do
Enum.each(topics[topic] || [], fn socket ->
send(socket.transport_pid, {:text, represent_conversation(participation)})
end)
end
def push_to_socket(topics, topic, %Activity{
data: %{"type" => "Delete", "deleted_activity_id" => deleted_activity_id}
}) do

View file

@ -352,7 +352,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
def delete_account(%{assigns: %{user: user}} = conn, params) do
case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
{:ok, user} ->
Task.start(fn -> User.delete(user) end)
User.delete(user)
json(conn, %{status: "success"})
{:error, msg} ->

View file

@ -293,7 +293,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
end
def get_external_profile(for_user, uri) do
with %User{} = user <- User.get_or_fetch(uri) do
with {:ok, %User{} = user} <- User.get_or_fetch(uri) do
{:ok, UserView.render("show.json", %{user: user, for: for_user})}
else
_e ->

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
alias Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Formatter
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
@ -181,6 +182,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|> Map.put("blocking_user", user)
|> Map.put("user", user)
|> Map.put(:visibility, "direct")
|> Map.put(:order, :desc)
activities =
ActivityPub.fetch_activities_query([user.ap_id], params)
@ -653,7 +655,22 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
defp parse_profile_bio(user, params) do
if bio = params["description"] do
Map.put(params, "bio", User.parse_bio(bio, user))
emojis_text = (params["description"] || "") <> " " <> (params["name"] || "")
emojis =
((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
|> Enum.dedup()
user_info =
user.info
|> Map.put(
"emoji",
emojis
)
params
|> Map.put("bio", User.parse_bio(bio, user))
|> Map.put("info", user_info)
else
params
end

View file

@ -67,6 +67,13 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
{String.trim(name, ":"), url}
end)
emoji = Enum.dedup(emoji ++ user.info.emoji)
description_html =
(user.bio || "")
|> HTML.filter_tags(User.html_filter_policy(for_user))
|> Formatter.emojify(emoji)
# ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
# For example: [{"name": "Pronoun", "value": "she/her"}, …]
fields =
@ -78,7 +85,7 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
%{
"created_at" => user.inserted_at |> Utils.format_naive_asctime(),
"description" => HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
"description_html" => HTML.filter_tags(user.bio, User.html_filter_policy(for_user)),
"description_html" => description_html,
"favourites_count" => 0,
"followers_count" => user_info[:follower_count],
"following" => following,