Commit b403ea4d authored by lain's avatar lain
Browse files

Merge branch 'develop' into dtluna/pleroma-feature/unfollow-activity

parents a9b2ad17 60b4b0d7
# This file contains the configuration for Credo and you are probably reading
# this after creating it with `mix credo.gen.config`.
#
# If you find anything wrong or unclear in this file, please report an
# issue on GitHub: https://github.com/rrrene/credo/issues
#
%{
#
# You can have as many configs as you like in the `configs:` field.
configs: [
%{
#
# Run any config using `mix credo -C <name>`. If no config name is given
# "default" is used.
name: "default",
#
# These are the files included in the analysis:
files: %{
#
# You can give explicit globs or simply directories.
# In the latter case `**/*.{ex,exs}` will be used.
included: ["lib/", "src/", "web/", "apps/"],
excluded: [~r"/_build/", ~r"/deps/"]
},
#
# If you create your own checks, you must specify the source files for
# them here, so they can be loaded by Credo before running the analysis.
requires: [],
#
# Credo automatically checks for updates, like e.g. Hex does.
# You can disable this behaviour below:
check_for_updates: true,
#
# If you want to enforce a style guide and need a more traditional linting
# experience, you can change `strict` to `true` below:
strict: false,
#
# If you want to use uncolored output by default, you can change `color`
# to `false` below:
color: true,
#
# You can customize the parameters of any check by adding a second element
# to the tuple.
#
# To disable a check put `false` as second element:
#
# {Credo.Check.Design.DuplicatedCode, false}
#
checks: [
{Credo.Check.Consistency.ExceptionNames},
{Credo.Check.Consistency.LineEndings},
{Credo.Check.Consistency.MultiAliasImportRequireUse},
{Credo.Check.Consistency.ParameterPatternMatching},
{Credo.Check.Consistency.SpaceAroundOperators},
{Credo.Check.Consistency.SpaceInParentheses},
{Credo.Check.Consistency.TabsOrSpaces},
# For some checks, like AliasUsage, you can only customize the priority
# Priority values are: `low, normal, high, higher`
{Credo.Check.Design.AliasUsage, priority: :low},
# For others you can set parameters
# If you don't want the `setup` and `test` macro calls in ExUnit tests
# or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just
# set the `excluded_macros` parameter to `[:schema, :setup, :test]`.
{Credo.Check.Design.DuplicatedCode, excluded_macros: []},
# You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero).
{Credo.Check.Design.TagTODO, exit_status: 2},
{Credo.Check.Design.TagFIXME},
{Credo.Check.Readability.FunctionNames},
{Credo.Check.Readability.LargeNumbers},
{Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 100},
{Credo.Check.Readability.ModuleAttributeNames},
{Credo.Check.Readability.ModuleDoc, false},
{Credo.Check.Readability.ModuleNames},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs},
{Credo.Check.Readability.ParenthesesInCondition},
{Credo.Check.Readability.PredicateFunctionNames},
{Credo.Check.Readability.PreferImplicitTry},
{Credo.Check.Readability.RedundantBlankLines},
{Credo.Check.Readability.StringSigils},
{Credo.Check.Readability.TrailingBlankLine},
{Credo.Check.Readability.TrailingWhiteSpace},
{Credo.Check.Readability.VariableNames},
{Credo.Check.Readability.Semicolons},
{Credo.Check.Readability.SpaceAfterCommas},
{Credo.Check.Refactor.DoubleBooleanNegation},
{Credo.Check.Refactor.CondStatements},
{Credo.Check.Refactor.CyclomaticComplexity},
{Credo.Check.Refactor.FunctionArity},
{Credo.Check.Refactor.MatchInCondition},
{Credo.Check.Refactor.NegatedConditionsInUnless},
{Credo.Check.Refactor.NegatedConditionsWithElse},
{Credo.Check.Refactor.Nesting},
{Credo.Check.Refactor.PipeChainStart},
{Credo.Check.Refactor.UnlessWithElse},
{Credo.Check.Warning.BoolOperationOnSameValues},
{Credo.Check.Warning.IExPry},
{Credo.Check.Warning.IoInspect},
{Credo.Check.Warning.LazyLogging},
{Credo.Check.Warning.OperationOnSameValues},
{Credo.Check.Warning.OperationWithConstantResult},
{Credo.Check.Warning.UnusedEnumOperation},
{Credo.Check.Warning.UnusedFileOperation},
{Credo.Check.Warning.UnusedKeywordOperation},
{Credo.Check.Warning.UnusedListOperation},
{Credo.Check.Warning.UnusedPathOperation},
{Credo.Check.Warning.UnusedRegexOperation},
{Credo.Check.Warning.UnusedStringOperation},
{Credo.Check.Warning.UnusedTupleOperation},
# Controversial and experimental checks (opt-in, just remove `, false`)
#
{Credo.Check.Refactor.ABCSize, false},
{Credo.Check.Refactor.AppendSingleItem, false},
{Credo.Check.Refactor.VariableRebinding, false},
{Credo.Check.Warning.MapGetUnsafePass, false},
# Deprecated checks (these will be deleted after a grace period)
{Credo.Check.Readability.Specs, false},
{Credo.Check.Warning.NameRedeclarationByAssignment, false},
{Credo.Check.Warning.NameRedeclarationByCase, false},
{Credo.Check.Warning.NameRedeclarationByDef, false},
{Credo.Check.Warning.NameRedeclarationByFn, false},
# Custom checks can be created using `mix credo.gen.check`.
#
]
}
]
}
- Add cache for user fetching / representing. (mostly in TwitterAPI.activity_to_status)
Unliking:
- Add a proper undo activity, find out how to ignore those in twitter api.
WEBSUB:
- Add unsubscription
- Add periodical renewal
......@@ -30,7 +30,9 @@
"application/xrd+xml" => ["xrd+xml"]
}
config :pleroma, :websub_verifier, Pleroma.Web.Websub
config :pleroma, :websub, Pleroma.Web.Websub
config :pleroma, :ostatus, Pleroma.Web.OStatus
config :pleroma, :httpoison, HTTPoison
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
......
......@@ -25,4 +25,6 @@
# Reduce hash rounds for testing
config :comeonin, :pbkdf2_rounds, 1
config :pleroma, :websub_verifier, Pleroma.Web.WebsubMock
config :pleroma, :websub, Pleroma.Web.WebsubMock
config :pleroma, :ostatus, Pleroma.Web.OStatusMock
config :pleroma, :httpoison, HTTPoisonMock
......@@ -5,6 +5,7 @@ defmodule Pleroma.Activity do
schema "activities" do
field :data, :map
field :local, :boolean, default: true
timestamps()
end
......@@ -18,4 +19,9 @@ def all_by_object_ap_id(ap_id) do
Repo.all(from activity in Activity,
where: fragment("? @> ?", activity.data, ^%{object: %{id: ap_id}}))
end
def get_create_activity_by_object_ap_id(ap_id) do
Repo.one(from activity in Activity,
where: fragment("? @> ?", activity.data, ^%{type: "Create", object: %{id: ap_id}}))
end
end
......@@ -15,10 +15,11 @@ def start(_type, _args) do
# Start your own worker by calling: Pleroma.Worker.start_link(arg1, arg2, arg3)
# worker(Pleroma.Worker, [arg1, arg2, arg3]),
worker(Cachex, [:user_cache, [
default_ttl: 5000,
default_ttl: 25000,
ttl_interval: 1000,
limit: 500
]])
limit: 2500
]]),
worker(Pleroma.Web.Federator, [])
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
......
......@@ -13,4 +13,24 @@ def get_by_ap_id(ap_id) do
Repo.one(from object in Object,
where: fragment("? @> ?", object.data, ^%{id: ap_id}))
end
def get_cached_by_ap_id(ap_id) do
if Mix.env == :test do
get_by_ap_id(ap_id)
else
key = "object:#{ap_id}"
Cachex.get!(:user_cache, key, fallback: fn(_) ->
object = get_by_ap_id(ap_id)
if object do
{:commit, object}
else
{:ignore, object}
end
end)
end
end
def context_mapping(context) do
%Object{data: %{"id" => context}}
end
end
defmodule Pleroma.Plugs.AuthenticationPlug do
alias Comeonin.Pbkdf2
import Plug.Conn
def init(options) do
......@@ -25,12 +26,12 @@ defp verify(%{id: id} = user, _password, id) do
end
defp verify(nil, _password, _user_id) do
Comeonin.Pbkdf2.dummy_checkpw
Pbkdf2.dummy_checkpw
:error
end
defp verify(user, password, _user_id) do
if Comeonin.Pbkdf2.checkpw(password, user.password_hash) do
if Pbkdf2.checkpw(password, user.password_hash) do
{:ok, user}
else
:error
......@@ -42,7 +43,7 @@ defp decode_header(conn) do
{:ok, userinfo} <- Base.decode64(header),
[username, password] <- String.split(userinfo, ":")
do
{ :ok, username, password }
{:ok, username, password}
end
end
......
defmodule Pleroma.Upload do
alias Ecto.UUID
alias Pleroma.Web
def store(%Plug.Upload{} = file) do
uuid = Ecto.UUID.generate
uuid = UUID.generate
upload_folder = Path.join(upload_path(), uuid)
File.mkdir_p!(upload_folder)
result_file = Path.join(upload_folder, file.filename)
......@@ -21,7 +23,7 @@ def store(%Plug.Upload{} = file) do
def store(%{"img" => "data:image/" <> image_data}) do
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
data = Base.decode64!(parsed["data"])
uuid = Ecto.UUID.generate
uuid = UUID.generate
upload_folder = Path.join(upload_path(), uuid)
File.mkdir_p!(upload_folder)
filename = Base.encode16(:crypto.hash(:sha256, data)) <> ".#{parsed["filetype"]}"
......@@ -44,11 +46,11 @@ def store(%{"img" => "data:image/" <> image_data}) do
end
defp upload_path do
Application.get_env(:pleroma, Pleroma.Upload)
|> Keyword.fetch!(:uploads)
settings = Application.get_env(:pleroma, Pleroma.Upload)
Keyword.fetch!(settings, :uploads)
end
defp url_for(file) do
"#{Pleroma.Web.base_url()}/media/#{file}"
"#{Web.base_url()}/media/#{file}"
end
end
defmodule Pleroma.User do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Pleroma.{Repo, User, Object}
import Ecto.{Changeset, Query}
alias Pleroma.{Repo, User, Object, Web}
alias Comeonin.Pbkdf2
alias Pleroma.Web.{OStatus, Websub}
alias Pleroma.Web.ActivityPub.ActivityPub
schema "users" do
......@@ -13,9 +15,11 @@ defmodule Pleroma.User do
field :password_hash, :string
field :password, :string, virtual: true
field :password_confirmation, :string, virtual: true
field :following, { :array, :string }, default: []
field :following, {:array, :string}, default: []
field :ap_id, :string
field :avatar, :map
field :local, :boolean, default: true
field :info, :map, default: %{}
timestamps()
end
......@@ -28,7 +32,7 @@ def avatar_url(user) do
end
def ap_id(%User{nickname: nickname}) do
"#{Pleroma.Web.base_url}/users/#{nickname}"
"#{Web.base_url}/users/#{nickname}"
end
def ap_followers(%User{} = user) do
......@@ -67,7 +71,7 @@ def register_changeset(struct, params \\ %{}) do
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
if changeset.valid? do
hashed = Comeonin.Pbkdf2.hashpwsalt(changeset.changes[:password])
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
changeset
......@@ -82,9 +86,13 @@ def register_changeset(struct, params \\ %{}) do
def follow(%User{} = follower, %User{} = followed) do
ap_followers = User.ap_followers(followed)
if following?(follower, followed) do
{ :error,
"Could not follow user: #{followed.nickname} is already on your list." }
{:error,
"Could not follow user: #{followed.nickname} is already on your list."}
else
if !followed.local do
Websub.subscribe(follower, followed)
end
following = [ap_followers | follower.following]
|> Enum.uniq
......@@ -105,7 +113,7 @@ def unfollow(%User{} = follower, %User{} = followed) do
|> Repo.update
{ :ok, follower, ActivityPub.fetch_latest_follow(follower, followed)}
else
{ :error, "Not subscribed!" }
{:error, "Not subscribed!"}
end
end
......@@ -120,6 +128,27 @@ def get_cached_by_ap_id(ap_id) do
def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
Cachex.get!(:user_cache, key, fallback: fn(_) -> Repo.get_by(User, nickname: nickname) end)
Cachex.get!(:user_cache, key, fallback: fn(_) -> get_or_fetch_by_nickname(nickname) end)
end
def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname)
end
def get_cached_user_info(user) do
key = "user_info:#{user.id}"
Cachex.get!(:user_cache, key, fallback: fn(_) -> user_info(user) end)
end
def get_or_fetch_by_nickname(nickname) do
with %User{} = user <- get_by_nickname(nickname) do
user
else _e ->
with [nick, domain] <- String.split(nickname, "@"),
{:ok, user} <- OStatus.make_user(nickname) do
user
else _e -> nil
end
end
end
end
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Repo
alias Pleroma.{Activity, Object, Upload, User}
alias Pleroma.{Activity, Repo, Object, Upload, User, Web}
alias Ecto.{Changeset, UUID}
import Ecto.Query
def insert(map) when is_map(map) do
def insert(map, local \\ true) when is_map(map) do
map = map
|> Map.put_new_lazy("id", &generate_activity_id/0)
|> Map.put_new_lazy("published", &make_date/0)
......@@ -16,10 +16,32 @@ def insert(map) when is_map(map) do
map
end
Repo.insert(%Activity{data: map})
Repo.insert(%Activity{data: map, local: local})
end
def like(%User{ap_id: ap_id} = user, %Object{data: %{ "id" => id}} = object) do
def create(to, actor, context, object, additional \\ %{}, published \\ nil, local \\ true) do
published = published || make_date()
activity = %{
"type" => "Create",
"to" => to |> Enum.uniq,
"actor" => actor.ap_id,
"object" => object,
"published" => published,
"context" => context
}
|> Map.merge(additional)
with {:ok, activity} <- insert(activity, local) do
if actor.local do
Pleroma.Web.Federator.enqueue(:publish, activity)
end
{:ok, activity}
end
end
def like(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object, local \\ true) do
cond do
# There's already a like here, so return the original activity.
ap_id in (object.data["likes"] || []) ->
......@@ -33,10 +55,11 @@ def like(%User{ap_id: ap_id} = user, %Object{data: %{ "id" => id}} = object) do
"type" => "Like",
"actor" => ap_id,
"object" => id,
"to" => [User.ap_followers(user), object.data["actor"]]
"to" => [User.ap_followers(user), object.data["actor"]],
"context" => object.data["context"]
}
{:ok, activity} = insert(data)
{:ok, activity} = insert(data, local)
likes = [ap_id | (object.data["likes"] || [])] |> Enum.uniq
......@@ -44,11 +67,15 @@ def like(%User{ap_id: ap_id} = user, %Object{data: %{ "id" => id}} = object) do
|> Map.put("like_count", length(likes))
|> Map.put("likes", likes)
changeset = Ecto.Changeset.change(object, data: new_data)
changeset = Changeset.change(object, data: new_data)
{:ok, object} = Repo.update(changeset)
update_object_in_activities(object)
if user.local do
Pleroma.Web.Federator.enqueue(:publish, activity)
end
{:ok, activity, object}
end
end
......@@ -58,7 +85,7 @@ defp update_object_in_activities(%{data: %{"id" => id}} = object) do
relevant_activities = Activity.all_by_object_ap_id(id)
Enum.map(relevant_activities, fn (activity) ->
new_activity_data = activity.data |> Map.put("object", object.data)
changeset = Ecto.Changeset.change(activity, data: new_activity_data)
changeset = Changeset.change(activity, data: new_activity_data)
Repo.update(changeset)
end)
end
......@@ -79,7 +106,7 @@ def unlike(%User{ap_id: ap_id}, %Object{data: %{ "id" => id}} = object) do
|> Map.put("like_count", length(likes))
|> Map.put("likes", likes)
changeset = Ecto.Changeset.change(object, data: new_data)
changeset = Changeset.change(object, data: new_data)
{:ok, object} = Repo.update(changeset)
update_object_in_activities(object)
......@@ -99,11 +126,11 @@ def generate_context_id do
end
def generate_object_id do
generate_id("objects")
Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :object, Ecto.UUID.generate)
end
def generate_id(type) do
"#{Pleroma.Web.base_url()}/#{type}/#{Ecto.UUID.generate}"
"#{Web.base_url()}/#{type}/#{UUID.generate}"
end
def fetch_public_activities(opts \\ %{}) do
......@@ -127,6 +154,12 @@ def fetch_activities(recipients, opts \\ %{}) do
query = from activity in query,
where: activity.id > ^since_id
query = if opts["local_only"] do
from activity in query, where: activity.local == true
else
query
end
query = if opts["max_id"] do
from activity in query, where: activity.id < ^opts["max_id"]
else
......@@ -140,19 +173,19 @@ def fetch_activities(recipients, opts \\ %{}) do
query
end
Repo.all(query)
|> Enum.reverse
Enum.reverse(Repo.all(query))
end
def announce(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object) do
def announce(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object, local \\ true) do
data = %{
"type" => "Announce",
"actor" => ap_id,
"object" => id,
"to" => [User.ap_followers(user), object.data["actor"]]
"to" => [User.ap_followers(user), object.data["actor"]],
"context" => object.data["context"]
}
{:ok, activity} = insert(data)
{:ok, activity} = insert(data, local)
announcements = [ap_id | (object.data["announcements"] || [])] |> Enum.uniq
......@@ -160,14 +193,56 @@ def announce(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object)
|> Map.put("announcement_count", length(announcements))
|> Map.put("announcements", announcements)
changeset = Ecto.Changeset.change(object, data: new_data)
changeset = Changeset.change(object, data: new_data)
{:ok, object} = Repo.update(changeset)
update_object_in_activities(object)
if user.local do
Pleroma.Web.Federator.enqueue(:publish, activity)
end
{:ok, activity, object}
end
def follow(%User{ap_id: follower_id, local: actor_local}, %User{ap_id: followed_id}, local \\ true) do
data = %{
"type" => "Follow",
"actor" => follower_id,
"to" => [followed_id],
"object" => followed_id,
"published" => make_date()
}
with {:ok, activity} <- insert(data, local) do
if actor_local do
Pleroma.Web.Federator.enqueue(:publish, activity)
end
{:ok, activity}
end
end
def unfollow(follower, followed, local \\ true) do
with follow_activity when not is_nil(follow_activity) <- fetch_latest_follow(follower, followed) do
data = %{
"type" => "Undo",
"actor" => follower.ap_id,
"to" => [followed.ap_id],
"object" => follow_activity.data["id"],
"published" => make_date()
}
with {:ok, activity} <- insert(data, local) do
if follower.local do
Pleroma.Web.Federator.enqueue(:publish, activity)
end
{:ok, activity}
end
end
end
def fetch_activities_for_context(context) do
query = from activity in Activity,
where: fragment("? @> ?", activity.data, ^%{ context: context })
......
defmodule Pleroma.Web.Federator do
use GenServer
alias Pleroma.User
alias Pleroma.Web.WebFinger
require Logger
@websub Application.get_env(:pleroma, :websub)
@ostatus Application.get_env(:pleroma, :ostatus)
<