Merge branch 'feature/activitypub' into 'develop'

Feature/activitypub

See merge request pleroma/pleroma!67
This commit is contained in:
lambda 2018-03-08 12:29:02 +00:00
commit 460062f2b0
79 changed files with 3485 additions and 169 deletions

View File

@ -27,7 +27,8 @@
metadata: [:request_id] metadata: [:request_id]
config :mime, :types, %{ config :mime, :types, %{
"application/xrd+xml" => ["xrd+xml"] "application/xrd+xml" => ["xrd+xml"],
"application/activity+json" => ["activity+json"]
} }
config :pleroma, :websub, Pleroma.Web.Websub config :pleroma, :websub, Pleroma.Web.Websub

View File

@ -7,7 +7,7 @@
# watchers to your application. For example, we use it # watchers to your application. For example, we use it
# with brunch.io to recompile .js and .css sources. # with brunch.io to recompile .js and .css sources.
config :pleroma, Pleroma.Web.Endpoint, config :pleroma, Pleroma.Web.Endpoint,
http: [port: 4000], http: [port: 4000, protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192]],
protocol: "http", protocol: "http",
debug_errors: true, debug_errors: true,
code_reloader: true, code_reloader: true,

View File

@ -0,0 +1,25 @@
defmodule Mix.Tasks.FixApUsers do
use Mix.Task
import Mix.Ecto
import Ecto.Query
alias Pleroma.{Repo, User}
@shortdoc "Grab all ap users again"
def run([]) do
Mix.Task.run("app.start")
q = from u in User,
where: fragment("? @> ?", u.info, ^%{"ap_enabled" => true}),
where: u.local == false
users = Repo.all(q)
Enum.each(users, fn(user) ->
try do
IO.puts("Fetching #{user.nickname}")
Pleroma.Web.ActivityPub.Transmogrifier.upgrade_user_from_ap_id(user.ap_id, false)
rescue
e -> IO.inspect(e)
end
end)
end
end

View File

@ -7,6 +7,7 @@ defmodule Pleroma.Activity do
field :data, :map field :data, :map
field :local, :boolean, default: true field :local, :boolean, default: true
field :actor, :string field :actor, :string
field :recipients, {:array, :string}
has_many :notifications, Notification, on_delete: :delete_all has_many :notifications, Notification, on_delete: :delete_all
timestamps() timestamps()

View File

@ -0,0 +1,27 @@
defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
alias Pleroma.Web.HTTPSignatures
import Plug.Conn
require Logger
def init(options) do
options
end
def call(%{assigns: %{valid_signature: true}} = conn, opts) do
conn
end
def call(conn, opts) do
user = conn.params["actor"]
Logger.debug("Checking sig for #{user}")
if get_req_header(conn, "signature") do
conn = conn
|> put_req_header("(request-target)", String.downcase("#{conn.method}") <> " #{conn.request_path}")
assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
else
Logger.debug("No signature header!")
conn
end
end
end

View File

@ -80,9 +80,15 @@ def remote_user_creation(params) do
|> validate_length(:name, max: 100) |> validate_length(:name, max: 100)
|> put_change(:local, false) |> put_change(:local, false)
if changes.valid? do if changes.valid? do
followers = User.ap_followers(%User{nickname: changes.changes[:nickname]}) case changes.changes[:info]["source_data"] do
changes %{"followers" => followers} ->
|> put_change(:follower_address, followers) changes
|> put_change(:follower_address, followers)
_ ->
followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
changes
|> put_change(:follower_address, followers)
end
else else
changes changes
end end
@ -97,6 +103,15 @@ def update_changeset(struct, params \\ %{}) do
|> validate_length(:name, min: 1, max: 100) |> validate_length(:name, min: 1, max: 100)
end end
def upgrade_changeset(struct, params \\ %{}) do
struct
|> cast(params, [:bio, :name, :info, :follower_address, :avatar])
|> unique_constraint(:nickname)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|> validate_length(:bio, max: 5000)
|> validate_length(:name, max: 100)
end
def password_update_changeset(struct, params) do def password_update_changeset(struct, params) do
changeset = struct changeset = struct
|> cast(params, [:password, :password_confirmation]) |> cast(params, [:password, :password_confirmation])
@ -144,11 +159,12 @@ def register_changeset(struct, params \\ %{}) do
def follow(%User{} = follower, %User{info: info} = followed) do def follow(%User{} = follower, %User{info: info} = followed) do
ap_followers = followed.follower_address ap_followers = followed.follower_address
if following?(follower, followed) or info["deactivated"] do if following?(follower, followed) or info["deactivated"] do
{:error, {:error,
"Could not follow user: #{followed.nickname} is already on your list."} "Could not follow user: #{followed.nickname} is already on your list."}
else else
if !followed.local && follower.local do if !followed.local && follower.local && !ap_enabled?(followed) do
Websub.subscribe(follower, followed) Websub.subscribe(follower, followed)
end end
@ -202,6 +218,11 @@ def update_and_set_cache(changeset) do
end end
end end
def invalidate_cache(user) do
Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
Cachex.del(:user_cache, "nickname:#{user.nickname}")
end
def get_cached_by_ap_id(ap_id) do def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}" key = "ap_id:#{ap_id}"
Cachex.get!(:user_cache, key, fallback: fn(_) -> get_by_ap_id(ap_id) end) Cachex.get!(:user_cache, key, fallback: fn(_) -> get_by_ap_id(ap_id) end)
@ -221,22 +242,30 @@ def get_cached_user_info(user) do
Cachex.get!(:user_cache, key, fallback: fn(_) -> user_info(user) end) Cachex.get!(:user_cache, key, fallback: fn(_) -> user_info(user) end)
end end
def fetch_by_nickname(nickname) do
ap_try = ActivityPub.make_user_from_nickname(nickname)
case ap_try do
{:ok, user} -> {:ok, user}
_ -> OStatus.make_user(nickname)
end
end
def get_or_fetch_by_nickname(nickname) do def get_or_fetch_by_nickname(nickname) do
with %User{} = user <- get_by_nickname(nickname) do with %User{} = user <- get_by_nickname(nickname) do
user user
else _e -> else _e ->
with [_nick, _domain] <- String.split(nickname, "@"), with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- OStatus.make_user(nickname) do {:ok, user} <- fetch_by_nickname(nickname) do
user user
else _e -> nil else _e -> nil
end end
end end
end end
# TODO: these queries could be more efficient if the type in postgresql wasn't map, but array.
def get_followers(%User{id: id, follower_address: follower_address}) do def get_followers(%User{id: id, follower_address: follower_address}) do
q = from u in User, q = from u in User,
where: fragment("? @> ?", u.following, ^follower_address ), where: ^follower_address in u.following,
where: u.id != ^id where: u.id != ^id
{:ok, Repo.all(q)} {:ok, Repo.all(q)}
@ -275,7 +304,7 @@ def update_note_count(%User{} = user) do
def update_follower_count(%User{} = user) do def update_follower_count(%User{} = user) do
follower_count_query = from u in User, follower_count_query = from u in User,
where: fragment("? @> ?", u.following, ^user.follower_address), where: ^user.follower_address in u.following,
where: u.id != ^user.id, where: u.id != ^user.id,
select: count(u.id) select: count(u.id)
@ -288,7 +317,7 @@ def update_follower_count(%User{} = user) do
update_and_set_cache(cs) update_and_set_cache(cs)
end end
def get_notified_from_activity(%Activity{data: %{"to" => to}}) do def get_notified_from_activity(%Activity{recipients: to}) do
query = from u in User, query = from u in User,
where: u.ap_id in ^to, where: u.ap_id in ^to,
where: u.local == true where: u.local == true
@ -296,10 +325,10 @@ def get_notified_from_activity(%Activity{data: %{"to" => to}}) do
Repo.all(query) Repo.all(query)
end end
def get_recipients_from_activity(%Activity{data: %{"to" => to}}) do def get_recipients_from_activity(%Activity{recipients: to}) do
query = from u in User, query = from u in User,
where: u.ap_id in ^to, where: u.ap_id in ^to,
or_where: fragment("? \\\?| ?", u.following, ^to) or_where: fragment("? && ?", u.following, ^to)
query = from u in query, query = from u in query,
where: u.local == true where: u.local == true
@ -376,4 +405,57 @@ def delete (%User{} = user) do
:ok :ok
end end
def get_or_fetch_by_ap_id(ap_id) do
if user = get_by_ap_id(ap_id) do
user
else
ap_try = ActivityPub.make_user_from_ap_id(ap_id)
case ap_try do
{:ok, user} -> user
_ ->
case OStatus.make_user(ap_id) do
{:ok, user} -> user
_ -> {:error, "Could not fetch by ap id"}
end
end
end
end
# AP style
def public_key_from_info(%{"source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
key = :public_key.pem_decode(public_key_pem)
|> hd()
|> :public_key.pem_entry_decode()
{:ok, key}
end
# OStatus Magic Key
def public_key_from_info(%{"magic_key" => magic_key}) do
{:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
end
def get_public_key_for_ap_id(ap_id) do
with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
{:ok, public_key} <- public_key_from_info(user.info) do
{:ok, public_key}
else
_ -> :error
end
end
defp blank?(""), do: nil
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)
end
def ap_enabled?(%User{info: info}), do: info["ap_enabled"]
def ap_enabled?(_), do: false
end end

View File

@ -1,14 +1,24 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.{Activity, Repo, Object, Upload, User, Notification} alias Pleroma.{Activity, Repo, Object, Upload, User, Notification}
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Federator
alias Pleroma.Web.OStatus
import Ecto.Query import Ecto.Query
import Pleroma.Web.ActivityPub.Utils import Pleroma.Web.ActivityPub.Utils
require Logger require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
def get_recipients(data) do
(data["to"] || []) ++ (data["cc"] || [])
end
def insert(map, local \\ true) when is_map(map) do def insert(map, local \\ true) when is_map(map) do
with nil <- Activity.get_by_ap_id(map["id"]), with nil <- Activity.get_by_ap_id(map["id"]),
map <- lazy_put_activity_defaults(map), map <- lazy_put_activity_defaults(map),
:ok <- insert_full_object(map) do :ok <- insert_full_object(map) do
{:ok, activity} = Repo.insert(%Activity{data: map, local: local, actor: map["actor"]}) {:ok, activity} = Repo.insert(%Activity{data: map, local: local, actor: map["actor"], recipients: get_recipients(map)})
Notification.create_notifications(activity) Notification.create_notifications(activity)
stream_out(activity) stream_out(activity)
{:ok, activity} {:ok, activity}
@ -30,7 +40,11 @@ def stream_out(activity) do
end end
end end
def create(to, actor, context, object, additional \\ %{}, published \\ nil, local \\ true) do def create(%{to: to, actor: actor, context: context, object: object} = params) do
additional = params[:additional] || %{}
local = !(params[:local] == false) # only accept false as false value
published = params[:published]
with create_data <- make_create_data(%{to: to, actor: actor, published: published, context: context, object: object}, additional), with create_data <- make_create_data(%{to: to, actor: actor, published: published, context: context, object: object}, additional),
{:ok, activity} <- insert(create_data, local), {:ok, activity} <- insert(create_data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
@ -38,6 +52,26 @@ def create(to, actor, context, object, additional \\ %{}, published \\ nil, loca
end end
end end
def accept(%{to: to, actor: actor, object: object} = params) do
local = !(params[:local] == false) # only accept false as false value
with data <- %{"to" => to, "type" => "Accept", "actor" => actor, "object" => object},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
local = !(params[:local] == false) # only accept false as false value
with data <- %{"to" => to, "cc" => cc, "type" => "Update", "actor" => actor, "object" => object},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
# TODO: This is weird, maybe we shouldn't check here if we can make the activity. # TODO: This is weird, maybe we shouldn't check here if we can make the activity.
def like(%User{ap_id: ap_id} = user, %Object{data: %{"id" => _}} = object, activity_id \\ nil, local \\ true) do def like(%User{ap_id: ap_id} = user, %Object{data: %{"id" => _}} = object, activity_id \\ nil, local \\ true) do
with nil <- get_existing_like(ap_id, object), with nil <- get_existing_like(ap_id, object),
@ -62,7 +96,8 @@ def unlike(%User{} = actor, %Object{} = object) do
end end
def announce(%User{ap_id: _} = user, %Object{data: %{"id" => _}} = object, activity_id \\ nil, local \\ true) do def announce(%User{ap_id: _} = user, %Object{data: %{"id" => _}} = object, activity_id \\ nil, local \\ true) do
with announce_data <- make_announce_data(user, object, activity_id), with true <- is_public?(object),
announce_data <- make_announce_data(user, object, activity_id),
{:ok, activity} <- insert(announce_data, local), {:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object), {:ok, object} <- add_announce_to_object(activity, object),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
@ -106,16 +141,26 @@ def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ tru
end end
def fetch_activities_for_context(context, opts \\ %{}) do def fetch_activities_for_context(context, opts \\ %{}) do
query = from activity in Activity, 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), where: fragment("?->>'type' = ? and ?->>'context' = ?", activity.data, "Create", activity.data, ^context),
order_by: [desc: :id] order_by: [desc: :id]
query = restrict_blocked(query, opts)
Repo.all(query) Repo.all(query)
end end
# TODO: Make this work properly with unlisted.
def fetch_public_activities(opts \\ %{}) do def fetch_public_activities(opts \\ %{}) do
public = ["https://www.w3.org/ns/activitystreams#Public"] q = fetch_activities_query(["https://www.w3.org/ns/activitystreams#Public"], opts)
fetch_activities(public, opts) q
|> Repo.all
|> Enum.reverse
end end
defp restrict_since(query, %{"since_id" => since_id}) do defp restrict_since(query, %{"since_id" => since_id}) do
@ -129,12 +174,15 @@ defp restrict_tag(query, %{"tag" => tag}) do
end end
defp restrict_tag(query, _), do: query defp restrict_tag(query, _), do: query
defp restrict_recipients(query, recipients) do defp restrict_recipients(query, [], user), do: query
Enum.reduce(recipients, query, fn (recipient, q) -> defp restrict_recipients(query, recipients, nil) do
map = %{ to: [recipient] } from activity in query,
from activity in q, where: fragment("? && ?", ^recipients, activity.recipients)
or_where: fragment(~s(? @> ?), activity.data, ^map) end
end) defp restrict_recipients(query, recipients, user) do
from activity in query,
where: fragment("? && ?", ^recipients, activity.recipients),
or_where: activity.actor == ^user.ap_id
end end
defp restrict_local(query, %{"local_only" => true}) do defp restrict_local(query, %{"local_only" => true}) do
@ -190,13 +238,13 @@ defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
end end
defp restrict_blocked(query, _), do: query defp restrict_blocked(query, _), do: query
def fetch_activities(recipients, opts \\ %{}) do def fetch_activities_query(recipients, opts \\ %{}) do
base_query = from activity in Activity, base_query = from activity in Activity,
limit: 20, limit: 20,
order_by: [fragment("? desc nulls last", activity.id)] order_by: [fragment("? desc nulls last", activity.id)]
base_query base_query
|> restrict_recipients(recipients) |> restrict_recipients(recipients, opts["user"])
|> restrict_tag(opts) |> restrict_tag(opts)
|> restrict_since(opts) |> restrict_since(opts)
|> restrict_local(opts) |> restrict_local(opts)
@ -207,6 +255,10 @@ def fetch_activities(recipients, opts \\ %{}) do
|> restrict_recent(opts) |> restrict_recent(opts)
|> restrict_blocked(opts) |> restrict_blocked(opts)
|> restrict_media(opts) |> restrict_media(opts)
end
def fetch_activities(recipients, opts \\ %{}) do
fetch_activities_query(recipients, opts)
|> Repo.all |> Repo.all
|> Enum.reverse |> Enum.reverse
end end
@ -215,4 +267,128 @@ def upload(file) do
data = Upload.store(file) data = Upload.store(file)
Repo.insert(%Object{data: data}) Repo.insert(%Object{data: data})
end end
def user_data_from_user_object(data) do
avatar = data["icon"]["url"] && %{
"type" => "Image",
"url" => [%{"href" => data["icon"]["url"]}]
}
banner = data["image"]["url"] && %{
"type" => "Image",
"url" => [%{"href" => data["image"]["url"]}]
}
user_data = %{
ap_id: data["id"],
info: %{
"ap_enabled" => true,
"source_data" => data,
"banner" => banner
},
avatar: avatar,
nickname: "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}",
name: data["name"],
follower_address: data["followers"],
bio: data["summary"]
}
{:ok, user_data}
end
def fetch_and_prepare_user_from_ap_id(ap_id) do
with {:ok, %{status_code: 200, body: body}} <- @httpoison.get(ap_id, ["Accept": "application/activity+json"]),
{:ok, data} <- Poison.decode(body) do
user_data_from_user_object(data)
else
e -> Logger.error("Could not user at fetch #{ap_id}, #{inspect(e)}")
end
end
def make_user_from_ap_id(ap_id) do
if user = User.get_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
User.insert_or_update_user(data)
else
e -> {:error, e}
end
end
end
def make_user_from_nickname(nickname) do
with {:ok, %{"ap_id" => ap_id}} when not is_nil(ap_id) <- WebFinger.finger(nickname) do
make_user_from_ap_id(ap_id)
else
_e -> {:error, "No ap id in webfinger"}
end
end
def publish(actor, activity) do
followers = if actor.follower_address in activity.recipients do
{:ok, followers} = User.get_followers(actor)
followers |> Enum.filter(&(!&1.local))
else
[]
end
remote_inboxes = (Pleroma.Web.Salmon.remote_users(activity) ++ followers)
|> Enum.filter(fn (user) -> User.ap_enabled?(user) end)
|> Enum.map(fn (%{info: %{"source_data" => data}}) ->
(data["endpoints"] && data["endpoints"]["sharedInbox"]) || data["inbox"]
end)
|> Enum.uniq
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
json = Poison.encode!(data)
Enum.each remote_inboxes, fn(inbox) ->
Federator.enqueue(:publish_single_ap, %{inbox: inbox, json: json, actor: actor, id: activity.data["id"]})
end
end
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
Logger.info("Federating #{id} to #{inbox}")
host = URI.parse(inbox).host
signature = Pleroma.Web.HTTPSignatures.sign(actor, %{host: host, "content-length": byte_size(json)})
@httpoison.post(inbox, json, [{"Content-Type", "application/activity+json"}, {"signature", signature}])
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
Logger.info("Fetching #{id} via AP")
with {:ok, %{body: body, status_code: code}} when code in 200..299 <- @httpoison.get(id, [Accept: "application/activity+json"], follow_redirect: true, timeout: 10000, recv_timeout: 20000),
{:ok, data} <- Poison.decode(body),
nil <- Object.get_by_ap_id(data["id"]),
params <- %{"type" => "Create", "to" => data["to"], "cc" => data["cc"], "actor" => data["attributedTo"], "object" => data},
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, Object.get_by_ap_id(activity.data["object"]["id"])}
else
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.get_by_ap_id(activity.data["object"]["id"])}
e -> e
end
end
end
end
def is_public?(activity) do
"https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++ (activity.data["cc"] || []))
end
def visible_for_user?(activity, nil) do
is_public?(activity)
end
def visible_for_user?(activity, user) do
x = [user.ap_id | user.following]
y = (activity.data["to"] ++ (activity.data["cc"] || []))
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
end
end end

View File

@ -0,0 +1,54 @@
defmodule Pleroma.Web.ActivityPub.ActivityPubController do
use Pleroma.Web, :controller
alias Pleroma.{User, Repo, Object, Activity}
alias Pleroma.Web.ActivityPub.{ObjectView, UserView, Transmogrifier}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.Federator
require Logger
action_fallback :errors
def user(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("user.json", %{user: user}))
end
end
def object(conn, %{"uuid" => uuid}) do
with ap_id <- o_status_url(conn, :object, uuid),
%Object{} = object <- Object.get_cached_by_ap_id(ap_id) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ObjectView.render("object.json", %{object: object}))
end
end
# TODO: Ensure that this inbox is a recipient of the message
def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
Federator.enqueue(:incoming_ap_doc, params)
json(conn, "ok")
end
def inbox(conn, params) do
headers = Enum.into(conn.req_headers, %{})
if !(String.contains?(headers["signature"] || "", params["actor"])) do
Logger.info("Signature not from author, relayed message, ignoring.")
else
Logger.info("Signature error.")
Logger.info("Could not validate #{params["actor"]}")
Logger.info(inspect(conn.req_headers))
end
json(conn, "ok")
end
def errors(conn, _e) do
conn
|> put_status(500)
|> json("error")
end
end

View File

@ -0,0 +1,298 @@
defmodule Pleroma.Web.ActivityPub.Transmogrifier do
@moduledoc """
A module to handle coding from internal to wire ActivityPub and back.
"""
alias Pleroma.User
alias Pleroma.Object
alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.ActivityPub
import Ecto.Query
require Logger
@doc """
Modifies an incoming AP object (mastodon format) to our internal format.
"""
def fix_object(object) do
object
|> Map.put("actor", object["attributedTo"])
|> fix_attachments
|> fix_context
|> fix_in_reply_to
end
def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object) when not is_nil(in_reply_to_id) do
case ActivityPub.fetch_object_from_id(in_reply_to_id) do
{:ok, replied_object} ->
activity = Activity.get_create_activity_by_object_ap_id(replied_object.data["id"])
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"])
e ->
Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}")
object
end
end
def fix_in_reply_to(object), do: object
def fix_context(object) do
object
|> Map.put("context", object["conversation"])
end
def fix_attachments(object) do
attachments = (object["attachment"] || [])
|> Enum.map(fn (data) ->
url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}]
Map.put(data, "url", url)
end)
object
|> Map.put("attachment", attachments)
end
# TODO: validate those with a Ecto scheme
# - tags
# - emoji
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]),
%User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
object = fix_object(data["object"])
params = %{
to: data["to"],
object: object,
actor: user,
context: object["conversation"],
local: false,
published: data["published"],
additional: Map.take(data, [
"cc",
"id"
])
}
ActivityPub.create(params)
else
%Activity{} = activity -> {:ok, activity}
_e -> :error
end
end
def handle_incoming(%{"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, activity} <- ActivityPub.follow(follower, followed, id, false) do
ActivityPub.accept(%{to: [follower.ap_id], actor: followed.ap_id, object: data, local: true})
User.follow(follower, followed)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(%{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = data) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity, object} <- ActivityPub.like(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity, object} <- ActivityPub.announce(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(%{"type" => "Update", "object" => %{"type" => "Person"} = object, "actor" => actor_id} = data) do
with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
banner = new_user_data[:info]["banner"]
update_data = new_user_data
|> Map.take([:name, :bio, :avatar])
|> Map.put(:info, Map.merge(actor.info, %{"banner" => banner}))
actor
|> User.upgrade_changeset(update_data)
|> User.update_and_set_cache()
ActivityPub.update(%{local: false, to: data["to"] || [], cc: data["cc"] || [], object: object, actor: actor_id})
else
e ->
Logger.error(e)
:error
end
end
# TODO: Make secure.
def handle_incoming(%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data) do
object_id = case object_id do
%{"id" => id} -> id
id -> id
end
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity} <- ActivityPub.delete(object, false) do
{:ok, activity}
else
e -> :error
end
end
# TODO
# Accept
# Undo
def handle_incoming(_), do: :error
def get_obj_helper(id) do
if object = Object.get_by_ap_id(id), do: {:ok, object}, else: nil
end
def prepare_object(object) do
object
|> set_sensitive
|> add_hashtags
|> add_mention_tags
|> add_attributed_to
|> prepare_attachments
|> set_conversation
end
@doc
"""
internal -> Mastodon
"""
def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
object = object
|> prepare_object
data = data
|> Map.put("object", object)
|> Map.put("@context", "https://www.w3.org/ns/activitystreams")
{:ok, data}
end
def prepare_outgoing(%{"type" => type} = data) do
data = data
|> Map.put("@context", "https://www.w3.org/ns/activitystreams")
{:ok, data}
end
def add_hashtags(object) do
tags = (object["tag"] || [])
|> Enum.map fn (tag) -> %{"href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}", "name" => "##{tag}", "type" => "Hashtag"} end
object
|> Map.put("tag", tags)
end
def add_mention_tags(object) do
recipients = object["to"] ++ (object["cc"] || [])
mentions = recipients
|> Enum.map(fn (ap_id) -> User.get_cached_by_ap_id(ap_id) end)
|> Enum.filter(&(&1))
|> Enum.map(fn(user) -> %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"} end)
tags = object["tag"] || []
object
|> Map.put("tag", tags ++ mentions)
end
def set_conversation(object) do
Map.put(object, "conversation", object["context"])
end
def set_sensitive(object) do
tags = object["tag"] || []
Map.put(object, "sensitive", "nsfw" in tags)
end
def add_attributed_to(object) do
attributedTo = object["attributedTo"] || object["actor"]
object
|> Map.put("attributedTo", attributedTo)
end
def prepare_attachments(object) do
attachments = (object["attachment"] || [])
|> Enum.map(fn (data) ->
[%{"mediaType" => media_type, "href" => href} | _] = data["url"]
%{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
end)
object
|> Map.put("attachment", attachments)
end
defp user_upgrade_task(user) do
old_follower_address = User.ap_followers(user)
q = from u in User,
where: ^old_follower_address in u.following,
update: [set: [following: fragment("array_replace(?,?,?)", u.following, ^old_follower_address, ^user.follower_address)]]
Repo.update_all(q, [])
maybe_retire_websub(user.ap_id)
# Only do this for recent activties, don't go through the whole db.
since = (Repo.aggregate(Activity, :max, :id) || 0) - 100_000
q = from a in Activity,
where: ^old_follower_address in a.recipients,
where: a.id > ^since,
update: [set: [recipients: fragment("array_replace(?,?,?)", a.recipients, ^old_follower_address, ^user.follower_address)]]
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
data = data
|> Map.put(:info, Map.merge(user.info, data[:info]))
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
end
{:ok, user}
else
e -> e
end
end
def maybe_retire_websub(ap_id) do
# some sanity checks
if is_binary(ap_id) && (String.length(ap_id) > 8) do
q = from ws in Pleroma.Web.Websub.WebsubClientSubscription,
where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
Repo.delete_all(q)
end
end
end

View File

@ -68,7 +68,7 @@ def lazy_put_object_defaults(map) do
@doc """ @doc """
Inserts a full object if it is contained in an activity. Inserts a full object if it is contained in an activity.
""" """
def insert_full_object(%{"object" => object_data}) when is_map(object_data) do def insert_full_object(%{"object" => %{"type" => type} = object_data}) when is_map(object_data) and type in ["Note"] do
with {:ok, _} <- Object.create(object_data) do with {:ok, _} <- Object.create(object_data) do
:ok :ok
end end
@ -109,6 +109,7 @@ def make_like_data(%User{ap_id: ap_id} = actor, %{data: %{"id" => id}} = object,
"actor" => ap_id, "actor" => ap_id,
"object" => id, "object" => id,
"to" => [actor.follower_address, object.data["actor"]], "to" => [actor.follower_address, object.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"context" => object.data["context"] "context" => object.data["context"]
} }
@ -150,6 +151,7 @@ def make_follow_data(%User{ap_id: follower_id}, %User{ap_id: followed_id}, activ
"type" => "Follow", "type" => "Follow",
"actor" => follower_id, "actor" => follower_id,
"to" => [followed_id], "to" => [followed_id],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => followed_id "object" => followed_id
} }
@ -177,6 +179,7 @@ def make_announce_data(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}}
"actor" => ap_id, "actor" => ap_id,
"object" => id, "object" => id,
"to" => [user.follower_address, object.data["actor"]], "to" => [user.follower_address, object.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"context" => object.data["context"] "context" => object.data["context"]
} }
@ -205,7 +208,6 @@ def make_unfollow_data(follower, followed, follow_activity) do
def make_create_data(params, additional) do def make_create_data(params, additional) do
published = params.published || make_date() published = params.published || make_date()
%{ %{
"type" => "Create", "type" => "Create",
"to" => params.to |> Enum.uniq, "to" => params.to |> Enum.uniq,

View File

@ -0,0 +1,27 @@
defmodule Pleroma.Web.ActivityPub.ObjectView do
use Pleroma.Web, :view
alias Pleroma.Web.ActivityPub.Transmogrifier
def render("object.json", %{object: object}) do
base = %{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
%{
"manuallyApprovesFollowers" => "as:manuallyApprovesFollowers",
"sensitive" => "as:sensitive",
"Hashtag" => "as:Hashtag",
"ostatus" => "http://ostatus.org#",
"atomUri" => "ostatus:atomUri",
"inReplyToAtomUri" => "ostatus:inReplyToAtomUri",
"conversation" => "ostatus:conversation",
"toot" => "http://joinmastodon.org/ns#",
"Emoji" => "toot:Emoji"
}
]
}
additional = Transmogrifier.prepare_object(object.data)
Map.merge(base, additional)
end
end

View File

@ -0,0 +1,57 @@
defmodule Pleroma.Web.ActivityPub.UserView do
use Pleroma.Web, :view
alias Pleroma.Web.Salmon
alias Pleroma.Web.WebFinger
alias Pleroma.User
def render("user.json", %{user: user}) do
{:ok, user} = WebFinger.ensure_keys_present(user)
{:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"])
public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)
public_key = :public_key.pem_encode([public_key])
%{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
%{
"manuallyApprovesFollowers" => "as:manuallyApprovesFollowers",
"sensitive" => "as:sensitive",
"Hashtag" => "as:Hashtag",
"ostatus" => "http://ostatus.org#",
"atomUri" => "ostatus:atomUri",
"inReplyToAtomUri" => "ostatus:inReplyToAtomUri",
"conversation" => "ostatus:conversation",
"toot" => "http://joinmastodon.org/ns#",
"Emoji" => "toot:Emoji"
}
],
"id" => user.ap_id,
"type" => "Person",
"following" => "#{user.ap_id}/following",
"followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox",
"outbox" => "#{user.ap_id}/outbox",
"preferredUsername" => user.nickname,
"name" => user.name,
"summary" => user.bio,
"url" => user.ap_id,
"manuallyApprovesFollowers" => false,
"publicKey" => %{
"id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
"endpoints" => %{
"sharedInbox" => "#{Pleroma.Web.Endpoint.url}/inbox"
},
"icon" => %{
"type" => "Image",
"url" => User.avatar_url(user)
},
"image" => %{
"type" => "Image",
"url" => User.banner_url(user)
}
}
end
end

View File

@ -46,24 +46,36 @@ def unfavorite(id_or_ap_id, user) do
end end
end end
def get_visibility(%{"visibility" => visibility}), do: visibility
def get_visibility(%{"in_reply_to_status_id" => status_id}) when status_id do
inReplyTo = get_replied_to_activity(status_id)
Pleroma.Web.MastodonAPI.StatusView.get_visibility(inReplyTo.data["object"])
end
def get_visibility(_), do: "public"
@instance Application.get_env(:pleroma, :instance) @instance Application.get_env(:pleroma, :instance)
@limit Keyword.get(@instance, :limit) @limit Keyword.get(@instance, :limit)
def post(user, %{"status" => status} = data) do def post(user, %{"status" => status} = data) do
visibility = get_visibility(data)
with status <- String.trim(status), with status <- String.trim(status),
length when length in 1..@limit <- String.length(status), length when length in 1..@limit <- String.length(status),
attachments <- attachments_from_ids(data["media_ids"]), attachments <- attachments_from_ids(data["media_ids"]),
mentions <- Formatter.parse_mentions(status), mentions <- Formatter.parse_mentions(status),
inReplyTo <- get_replied_to_activity(data["in_reply_to_status_id"]), inReplyTo <- get_replied_to_activity(data["in_reply_to_status_id"]),
to <- to_for_user_and_mentions(user, mentions, inReplyTo), {to, cc} <- to_for_user_and_mentions(user, mentions, inReplyTo, visibility),
tags <- Formatter.parse_tags(status, data), tags <- Formatter.parse_tags(status, data),
content_html <- make_content_html(status, mentions, attachments, tags, data["no_attachment_links"]), content_html <- make_content_html(status, mentions, attachments, tags, data["no_attachment_links"]),
context <- make_context(inReplyTo), context <- make_context(inReplyTo),
cw <- data["spoiler_text"], cw <- data["spoiler_text"],
object <- make_note_data(user.ap_id, to, context, content_html, attachments, inReplyTo, tags, cw), object <- make_note_data(user.ap_id, to, context, content_html, attachments, inReplyTo, tags, cw, cc),
object <- Map.put(object, "emoji", Formatter.get_emoji(status) |> Enum.reduce(%{}, fn({name, file}, acc) -> Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url}#{file}") end)) do object <- Map.put(object, "emoji", Formatter.get_emoji(status) |> Enum.reduce(%{}, fn({name, file}, acc) -> Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url}#{file}") end)) do
res = ActivityPub.create(to, user, context, object) res = ActivityPub.create(%{to: to, actor: user, context: context, object: object, additional: %{"cc" => cc}})
User.increase_note_count(user) User.increase_note_count(user)
res res
end end
end end
def update(user) do
ActivityPub.update(%{local: true, to: [user.follower_address], cc: [], actor: user.ap_id, object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})})
end
end end

View File

@ -24,17 +24,34 @@ def attachments_from_ids(ids) do
end) end)
end end
def to_for_user_and_mentions(user, mentions, inReplyTo) do def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
default_to = [ to = ["https://www.w3.org/ns/activitystreams#Public"]
user.follower_address,
"https://www.w3.org/ns/activitystreams#Public"
]
to = default_to ++ Enum.map(mentions, fn ({_, %{ap_id: ap_id}}) -> ap_id end) mentioned_users = Enum.map(mentions, fn ({_, %{ap_id: ap_id}}) -> ap_id end)
cc = [user.follower_address | mentioned_users]
if inReplyTo do if inReplyTo do
Enum.uniq([inReplyTo.data["actor"] | to]) {to, Enum.uniq([inReplyTo.data["actor"] | cc])}
else else
to {to, cc}
end
end
def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do
{to, cc} = to_for_user_and_mentions(user, mentions, inReplyTo, "public")
{cc, to}
end
def to_for_user_and_mentions(user, mentions, inReplyTo, "private") do
{to, cc} = to_for_user_and_mentions(user, mentions, inReplyTo, "direct")
{[user.follower_address | to], cc}
end
def to_for_user_and_mentions(user, mentions, inReplyTo, "direct") do
mentioned_users = Enum.map(mentions, fn ({_, %{ap_id: ap_id}}) -> ap_id end)
if inReplyTo do
{Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
else
{mentioned_users, []}
end end
end end
@ -99,10 +116,11 @@ def add_user_links(text, mentions) do
end) end)
end end
def make_note_data(actor, to, context, content_html, attachments, inReplyTo, tags, cw \\ nil) do def make_note_data(actor, to, context, content_html, attachments, inReplyTo, tags, cw \\ nil, cc \\ []) do
object = %{ object = %{
"type" => "Note", "type" => "Note",
"to" => to, "to" => to,
"cc" => cc,
"content" => content_html, "content" => content_html,
"summary" => cw, "summary" => cw,
"context" => context, "context" => context,

View File

@ -1,7 +1,10 @@
defmodule Pleroma.Web.Federator do defmodule Pleroma.Web.Federator do
use GenServer use GenServer
alias Pleroma.User alias Pleroma.User
alias Pleroma.Activity
alias Pleroma.Web.{WebFinger, Websub} alias Pleroma.Web.{WebFinger, Websub}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
require Logger require Logger
@websub Application.get_env(:pleroma, :websub) @websub Application.get_env(:pleroma, :websub)
@ -44,11 +47,16 @@ def handle(:publish, activity) do
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end) Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do
{:ok, actor} = WebFinger.ensure_keys_present(actor) {:ok, actor} = WebFinger.ensure_keys_present(actor)
Logger.debug(fn -> "Sending #{activity.data["id"]} out via salmon" end) if ActivityPub.is_public?(activity) do
Pleroma.Web.Salmon.publish(actor, activity) Logger.info(fn -> "Sending #{activity.data["id"]} out via websub" end)
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
Logger.debug(fn -> "Sending #{activity.data["id"]} out via websub" end) Logger.info(fn -> "Sending #{activity.data["id"]} out via salmon" end)
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity) Pleroma.Web.Salmon.publish(actor, activity)
end
Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end)
Pleroma.Web.ActivityPub.ActivityPub.publish(actor, activity)
end end
end end
@ -58,10 +66,29 @@ def handle(:verify_websub, websub) do
end end
def handle(:incoming_doc, doc) do def handle(:incoming_doc, doc) do
Logger.debug("Got document, trying to parse") Logger.info("Got document, trying to parse")
@ostatus.handle_incoming(doc) @ostatus.handle_incoming(doc)
end end
def handle(:incoming_ap_doc, params) do
Logger.info("Handling incoming ap activity")
with {:ok, _user} <- ap_enabled_actor(params["actor"]),
nil <- Activity.get_by_ap_id(params["id"]),
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
else
%Activity{} ->
Logger.info("Already had #{params["id"]}")
e ->
# Just drop those for now
Logger.info("Unhandled activity")
Logger.info(Poison.encode!(params, [pretty: 2]))
end
end
def handle(:publish_single_ap, params) do
ActivityPub.publish_one(params)
end
def handle(:publish_single_websub, %{xml: xml, topic: topic, callback: callback, secret: secret}) do def handle(:publish_single_websub, %{xml: xml, topic: topic, callback: callback, secret: secret}) do
signature = @websub.sign(secret || "", xml) signature = @websub.sign(secret || "", xml)
Logger.debug(fn -> "Pushing #{topic} to #{callback}" end) Logger.debug(fn -> "Pushing #{topic} to #{callback}" end)
@ -102,7 +129,7 @@ def maybe_start_job(running_jobs, queue) do
end end
end end
def handle_cast({:enqueue, type, payload, priority}, state) when type in [:incoming_doc] do def handle_cast({:enqueue, type, payload, priority}, state) when type in [:incoming_doc, :incoming_ap_doc] do
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
i_queue = enqueue_sorted(i_queue, {type, payload}, 1) i_queue = enqueue_sorted(i_queue, {type, payload}, 1)
{i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue) {i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue)
@ -139,4 +166,13 @@ def enqueue_sorted(queue, element, priority) do
def queue_pop([%{item: element} | queue]) do def queue_pop([%{item: element} | queue]) do
{element, queue} {element, queue}
end end
def ap_enabled_actor(id) do
user = User.get_by_ap_id(id)
if User.ap_enabled?(user) do
{:ok, user}
else
ActivityPub.make_user_from_ap_id(id)
end
end
end end

View File

@ -0,0 +1,79 @@
# https://tools.ietf.org/html/draft-cavage-http-signatures-08
defmodule Pleroma.Web.HTTPSignatures do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
require Logger
def split_signature(sig) do
default = %{"headers" => "date"}
sig = sig
|> String.trim()
|> String.split(",")
|> Enum.reduce(default, fn(part, acc) ->
[key | rest] = String.split(part, "=")
value = Enum.join(rest, "=")
Map.put(acc, key, String.trim(value, "\""))
end)
Map.put(sig, "headers", String.split(sig["headers"], ~r/\s/))
end
def validate(headers, signature, public_key) do
sigstring = build_signing_string(headers, signature["headers"])
{:ok, sig} = Base.decode64(signature["signature"])
:public_key.verify(sigstring, :sha256, sig, public_key)
end
def validate_conn(conn) do
# TODO: How to get the right key and see if it is actually valid for that request.
# For now, fetch the key for the actor.
with actor_id <- conn.params["actor"],
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
if validate_conn(conn, public_key) do
true
else
Logger.debug("Could not validate, re-fetching user and trying one more time.")
# Fetch user anew and try one more time
with actor_id <- conn.params["actor"],
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
validate_conn(conn, public_key)
end
end
else
e ->
Logger.debug("Could not public key!")
end
end
def validate_conn(conn, public_key) do
headers = Enum.into(conn.req_headers, %{})
signature = split_signature(headers["signature"])
validate(headers, signature, public_key)
end
def build_signing_string(headers, used_headers) do
used_headers
|> Enum.map(fn (header) -> "#{header}: #{headers[header]}" end)
|> Enum.join("\n")
end
def sign(user, headers) do
with {:ok, %{info: %{"keys" => keys}}} <- Pleroma.Web.WebFinger.ensure_keys_present(user),
{:ok, private_key, _} = Pleroma.Web.Salmon.keys_from_pem(keys) do
sigstring = build_signing_string(headers, Map.keys(headers))
signature = :public_key.sign(sigstring, :sha256, private_key)
|> Base.encode64()
[
keyId: user.ap_id <> "#main-key",
algorithm: "rsa-sha256",
headers: Map.keys(headers) |> Enum.join(" "),
signature: signature
]
|> Enum.map(fn({k, v}) -> "#{k}=\"#{v}\"" end)
|> Enum.join(",")
end
end
end

View File

@ -24,6 +24,7 @@ def create_app(conn, params) do
end end
def update_credentials(%{assigns: %{user: user}} = conn, params) do def update_credentials(%{assigns: %{user: user}} = conn, params) do
original_user = user
params = if bio = params["note"] do params = if bio = params["note"] do
Map.put(params, "bio", bio) Map.put(params, "bio", bio)
else else
@ -40,7 +41,7 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
with %Plug.Upload{} <- avatar, with %Plug.Upload{} <- avatar,
{:ok, object} <- ActivityPub.upload(avatar), {:ok, object} <- ActivityPub.upload(avatar),
change = Ecto.Changeset.change(user, %{avatar: object.data}), change = Ecto.Changeset.change(user, %{avatar: object.data}),
{:ok, user} = Repo.update(change) do {:ok, user} = User.update_and_set_cache(change) do
user user
else else
_e -> user _e -> user
@ -54,7 +55,7 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
{:ok, object} <- ActivityPub.upload(banner), {:ok, object} <- ActivityPub.upload(banner),
new_info <- Map.put(user.info, "banner", object.data), new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}), change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- Repo.update(change) do {:ok, user} <- User.update_and_set_cache(change) do
user user
else else
_e -> user _e -> user
@ -64,7 +65,10 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
end end
with changeset <- User.update_changeset(user, params), with changeset <- User.update_changeset(user, params),
{:ok, user} <- Repo.update(changeset) do {:ok, user} <- User.update_and_set_cache(changeset) do
if original_user != user do
CommonAPI.update(user)
end
json conn, AccountView.render("account.json", %{user: user}) json conn, AccountView.render("account.json", %{user: user})
else else
_e -> _e ->
@ -150,6 +154,7 @@ def home_timeline(%{assigns: %{user: user}} = conn, params) do
params = params params = params
|> Map.put("type", ["Create", "Announce"]) |> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> Map.put("user", user)
activities = ActivityPub.fetch_activities([user.ap_id | user.following], params) activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
|> Enum.reverse |> Enum.reverse
@ -181,7 +186,7 @@ def user_statuses(%{assigns: %{user: user}} = conn, params) do
|> Map.put("actor_id", ap_id) |> Map.put("actor_id", ap_id)
|> Map.put("whole_db", true) |> Map.put("whole_db", true)
activities = ActivityPub.fetch_activities([], params) activities = ActivityPub.fetch_public_activities(params)
|> Enum.reverse |> Enum.reverse
render conn, StatusView, "index.json", %{activities: activities, for: user, as: :activity} render conn, StatusView, "index.json", %{activities: activities, for: user, as: :activity}
@ -189,14 +194,15 @@ def user_statuses(%{assigns: %{user: user}} = conn, params) do
end end
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id) do with %Activity{} = activity <- Repo.get(Activity, id),
true <- ActivityPub.visible_for_user?(activity, user) do
render conn, StatusView, "status.json", %{activity: activity, for: user} render conn, StatusView, "status.json", %{activity: activity, for: user}
end end
end end
def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id), with %Activity{} = activity <- Repo.get(Activity, id),
activities <- ActivityPub.fetch_activities_for_context(activity.data["object"]["context"], %{"blocking_user" => user}), activities <- ActivityPub.fetch_activities_for_context(activity.data["context"], %{"blocking_user" => user, "user" => user}),
activities <- activities |> Enum.filter(fn (%{id: aid}) -> to_string(aid) != to_string(id) end), activities <- activities |> Enum.filter(fn (%{id: aid}) -> to_string(aid) != to_string(id) end),
activities <- activities |> Enum.filter(fn (%{data: %{"type" => type}}) -> type == "Create" end), activities <- activities |> Enum.filter(fn (%{data: %{"type" => type}}) -> type == "Create" end),
grouped_activities <- Enum.group_by(activities, fn (%{id: id}) -> id < activity.id end) do grouped_activities <- Enum.group_by(activities, fn (%{id: id}) -> id < activity.id end) do
@ -463,12 +469,12 @@ def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) d
end end
def favourites(%{assigns: %{user: user}} = conn, _) do def favourites(%{assigns: %{user: user}} = conn, _) do
params = conn params = %{}
|> Map.put("type", "Create") |> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id) |> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
activities = ActivityPub.fetch_activities([], params) activities = ActivityPub.fetch_public_activities(params)
|> Enum.reverse |> Enum.reverse
conn conn

View File

@ -25,7 +25,6 @@ def connect(params, socket) do
def id(_), do: nil def id(_), do: nil
def handle(:text, message, _state) do def handle(:text, message, _state) do
IO.inspect message
#| :ok #| :ok
#| state #| state
#| {:text, message} #| {:text, message}

View File

@ -18,7 +18,7 @@ def render("account.json", %{user: user}) do
id: to_string(user.id), id: to_string(user.id),
username: hd(String.split(user.nickname, "@")), username: hd(String.split(user.nickname, "@")),
acct: user.nickname, acct: user.nickname,
display_name: user.name, display_name: user.name || user.nickname,
locked: false, locked: false,
created_at: Utils.to_masto_date(user.inserted_at), created_at: Utils.to_masto_date(user.inserted_at),
followers_count: user_info.follower_count, followers_count: user_info.follower_count,

View File

@ -58,7 +58,7 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
announcement_count = object["announcement_count"] || 0 announcement_count = object["announcement_count"] || 0
tags = object["tag"] || [] tags = object["tag"] || []
sensitive = Enum.member?(tags, "nsfw") sensitive = object["sensitive"] || Enum.member?(tags, "nsfw")
mentions = activity.data["to"] mentions = activity.data["to"]
|> Enum.map(fn (ap_id) -> User.get_cached_by_ap_id(ap_id) end) |> Enum.map(fn (ap_id) -> User.get_cached_by_ap_id(ap_id) end)
@ -96,7 +96,7 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
muted: false, muted: false,
sensitive: sensitive, sensitive: sensitive,
spoiler_text: object["summary"] || "", spoiler_text: object["summary"] || "",
visibility: "public", visibility: get_visibility(object),
media_attachments: attachments |> Enum.take(4), media_attachments: attachments |> Enum.take(4),
mentions: mentions, mentions: mentions,
tags: [], # fix, tags: [], # fix,
@ -109,6 +109,18 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
} }
end end
def get_visibility(object) do
public = "https://www.w3.org/ns/activitystreams#Public"
to = object["to"] || []
cc = object["cc"] || []
cond do
public in to -> "public"
public in cc -> "unlisted"
Enum.any?(to, &(String.contains?(&1, "/followers"))) -> "private"
true -> "direct"
end
end
def render("attachment.json", %{attachment: attachment}) do def render("attachment.json", %{attachment: attachment}) do
[%{"mediaType" => media_type, "href" => href} | _] = attachment["url"] [%{"mediaType" => media_type, "href" => href} | _] = attachment["url"]

View File

@ -76,10 +76,17 @@ def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user,
in_reply_to = get_in_reply_to(activity.data) in_reply_to = get_in_reply_to(activity.data)
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
mentions = activity.data["to"] |> get_mentions mentions = activity.recipients |> get_mentions
categories = (activity.data["object"]["tag"] || []) categories = (activity.data["object"]["tag"] || [])
|> Enum.map(fn (tag) -> {:category, [term: to_charlist(tag)], []} end) |> Enum.map(fn (tag) ->
if is_binary(tag) do
{:category, [term: to_charlist(tag)], []}
else
nil
end
end)
|> Enum.filter(&(&1))
emoji_links = get_emoji_links(activity.data["object"]["emoji"] || %{}) emoji_links = get_emoji_links(activity.data["object"]["emoji"] || %{})
@ -110,7 +117,7 @@ def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) d
_in_reply_to = get_in_reply_to(activity.data) _in_reply_to = get_in_reply_to(activity.data)
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
mentions = activity.data["to"] |> get_mentions mentions = activity.recipients |> get_mentions
[ [
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/favorite']}, {:"activity:verb", ['http://activitystrea.ms/schema/1.0/favorite']},
@ -144,7 +151,7 @@ def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_autho
retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true) retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true)
mentions = activity.data["to"] |> get_mentions mentions = activity.recipients |> get_mentions
[ [
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']}, {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/share']}, {:"activity:verb", ['http://activitystrea.ms/schema/1.0/share']},
@ -168,7 +175,7 @@ def to_simple_form(%{data: %{"type" => "Follow"}} = activity, user, with_author)
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
mentions = (activity.data["to"] || []) |> get_mentions mentions = (activity.recipients || []) |> get_mentions
[ [
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']}, {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/follow']}, {:"activity:verb", ['http://activitystrea.ms/schema/1.0/follow']},
@ -196,7 +203,7 @@ def to_simple_form(%{data: %{"type" => "Undo"}} = activity, user, with_author) d
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
follow_activity = Activity.get_by_ap_id(activity.data["object"]) follow_activity = Activity.get_by_ap_id(activity.data["object"])
mentions = (activity.data["to"] || []) |> get_mentions mentions = (activity.recipients || []) |> get_mentions
[ [
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']}, {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/unfollow']}, {:"activity:verb", ['http://activitystrea.ms/schema/1.0/unfollow']},

View File

@ -88,6 +88,7 @@ def fetch_replied_to_activity(entry, inReplyTo) do
end end
end end
# TODO: Clean this up a bit.
def handle_note(entry, doc \\ nil) do def handle_note(entry, doc \\ nil) do
with id <- XML.string_from_xpath("//id", entry), with id <- XML.string_from_xpath("//id", entry),
activity when is_nil(activity) <- Activity.get_create_activity_by_object_ap_id(id), activity when is_nil(activity) <- Activity.get_create_activity_by_object_ap_id(id),
@ -104,15 +105,18 @@ def handle_note(entry, doc \\ nil) do
mentions <- get_mentions(entry), mentions <- get_mentions(entry),
to <- make_to_list(actor, mentions), to <- make_to_list(actor, mentions),
date <- XML.string_from_xpath("//published", entry), date <- XML.string_from_xpath("//published", entry),
unlisted <- XML.string_from_xpath("//mastodon:scope", entry) == "unlisted",
cc <- if(unlisted, do: ["https://www.w3.org/ns/activitystreams#Public"], else: []),
note <- CommonAPI.Utils.make_note_data(actor.ap_id, to, context, content_html, attachments, inReplyToActivity, [], cw), note <- CommonAPI.Utils.make_note_data(actor.ap_id, to, context, content_html, attachments, inReplyToActivity, [], cw),
note <- note |> Map.put("id", id) |> Map.put("tag", tags), note <- note |> Map.put("id", id) |> Map.put("tag", tags),
note <- note |> Map.put("published", date), note <- note |> Map.put("published", date),
note <- note |> Map.put("emoji", get_emoji(entry)), note <- note |> Map.put("emoji", get_emoji(entry)),
note <- add_external_url(note, entry), note <- add_external_url(note, entry),
note <- note |> Map.put("cc", cc),
# TODO: Handle this case in make_note_data # TODO: Handle this case in make_note_data
note <- (if inReplyTo && !inReplyToActivity, do: note |> Map.put("inReplyTo", inReplyTo), else: note) note <- (if inReplyTo && !inReplyToActivity, do: note |> Map.put("inReplyTo", inReplyTo), else: note)
do do
res = ActivityPub.create(to, actor, context, note, %{}, date, false) res = ActivityPub.create(%{to: to, actor: actor, context: context, object: note, published: date, local: false, additional: %{"cc" => cc}})
User.increase_note_count(actor) User.increase_note_count(actor)
res res
else else

View File

@ -9,6 +9,7 @@ defmodule Pleroma.Web.OStatus do
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.{WebFinger, Websub} alias Pleroma.Web.{WebFinger, Websub}
alias Pleroma.Web.OStatus.{FollowHandler, NoteHandler, DeleteHandler} alias Pleroma.Web.OStatus.{FollowHandler, NoteHandler, DeleteHandler}
alias Pleroma.Web.ActivityPub.Transmogrifier
def feed_path(user) do def feed_path(user) do
"#{user.ap_id}/feed.atom" "#{user.ap_id}/feed.atom"
@ -177,6 +178,13 @@ def get_tags(entry) do
end end
def maybe_update(doc, user) do def maybe_update(doc, user) do
if "true" == string_from_xpath("//author[1]/ap_enabled", doc) do
Transmogrifier.upgrade_user_from_ap_id(user.ap_id)
else
maybe_update_ostatus(doc, user)
end
end
def maybe_update_ostatus(doc, user) do
old_data = %{ old_data = %{
avatar: user.avatar, avatar: user.avatar,
bio: user.bio, bio: user.bio,
@ -218,11 +226,6 @@ def find_or_make_user(uri) do
end end
end end
def insert_or_update_user(data) do
cs = User.remote_user_creation(data)
Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
end
def make_user(uri, update \\ false) do def make_user(uri, update \\ false) do
with {:ok, info} <- gather_user_info(uri) do with {:ok, info} <- gather_user_info(uri) do
data = %{ data = %{
@ -236,7 +239,7 @@ def make_user(uri, update \\ false) do
with false <- update, with false <- update,
%User{} = user <- User.get_by_ap_id(data.ap_id) do %User{} = user <- User.get_by_ap_id(data.ap_id) do
{:ok, user} {:ok, user}
else _e -> insert_or_update_user(data) else _e -> User.insert_or_update_user(data)
end end
end end
end end
@ -297,7 +300,10 @@ def fetch_activity_from_atom_url(url) do
with {:ok, %{body: body, status_code: code}} when code in 200..299 <- @httpoison.get(url, [Accept: "application/atom+xml"], follow_redirect: true, timeout: 10000, recv_timeout: 20000) do with {:ok, %{body: body, status_code: code}} when code in 200..299 <- @httpoison.get(url, [Accept: "application/atom+xml"], follow_redirect: true, timeout: 10000, recv_timeout: 20000) do
Logger.debug("Got document from #{url}, handling...") Logger.debug("Got document from #{url}, handling...")
handle_incoming(body) handle_incoming(body)
else e -> Logger.debug("Couldn't get #{url}: #{inspect(e)}") else
e ->
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
e
end end
end end
@ -306,7 +312,10 @@ def fetch_activity_from_html_url(url) do
with {:ok, %{body: body}} <- @httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000), with {:ok, %{body: body}} <- @httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000),
{:ok, atom_url} <- get_atom_url(body) do {:ok, atom_url} <- get_atom_url(body) do
fetch_activity_from_atom_url(atom_url) fetch_activity_from_atom_url(atom_url)
else e -> Logger.debug("Couldn't get #{url}: #{inspect(e)}") else
e ->
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
e
end end
end end

View File

@ -6,27 +6,25 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Web.{OStatus, Federator} alias Pleroma.Web.{OStatus, Federator}
alias Pleroma.Web.XML alias Pleroma.Web.XML
alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.ActivityPub.ActivityPub
import Ecto.Query import Ecto.Query
def feed_redirect(conn, %{"nickname" => nickname}) do def feed_redirect(conn, %{"nickname" => nickname} = params) do
user = User.get_cached_by_nickname(nickname) user = User.get_cached_by_nickname(nickname)
case get_format(conn) do case get_format(conn) do
"html" -> Fallback.RedirectController.redirector(conn, nil) "html" -> Fallback.RedirectController.redirector(conn, nil)
"activity+json" -> ActivityPubController.user(conn, params)
_ -> redirect conn, external: OStatus.feed_path(user) _ -> redirect conn, external: OStatus.feed_path(user)
end end
end end
def feed(conn, %{"nickname" => nickname} = params) do def feed(conn, %{"nickname" => nickname} = params) do
user = User.get_cached_by_nickname(nickname) user = User.get_cached_by_nickname(nickname)
query = from activity in Activity,
where: fragment("?->>'actor' = ?", activity.data, ^user.ap_id),
limit: 20,
order_by: [desc: :id]
activities = query activities = ActivityPub.fetch_public_activities(%{"whole_db" => true, "actor_id" => user.ap_id})
|> restrict_max(params) |> Enum.reverse
|> Repo.all
response = user response = user
|> FeedRepresenter.to_simple_form(activities, [user]) |> FeedRepresenter.to_simple_form(activities, [user])
@ -55,11 +53,6 @@ defp decode_or_retry(body) do
end end
end end
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
def salmon_incoming(conn, _) do def salmon_incoming(conn, _) do
{:ok, body, _conn} = read_body(conn) {:ok, body, _conn} = read_body(conn)
{:ok, doc} = decode_or_retry(body) {:ok, doc} = decode_or_retry(body)
@ -70,17 +63,23 @@ def salmon_incoming(conn, _) do
|> send_resp(200, "") |> send_resp(200, "")
end end
def object(conn, %{"uuid" => uuid}) do # TODO: Data leak
with id <- o_status_url(conn, :object, uuid), def object(conn, %{"uuid" => uuid} = params) do
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id), if get_format(conn) == "activity+json" do
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do ActivityPubController.object(conn, params)
case get_format(conn) do else
"html" -> redirect(conn, to: "/notice/#{activity.id}") with id <- o_status_url(conn, :object, uuid),
_ -> represent_activity(conn, activity, user) %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id),
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case get_format(conn) do
"html" -> redirect(conn, to: "/notice/#{activity.id}")
_ -> represent_activity(conn, activity, user)
end
end end
end end
end end
# TODO: Data leak
def activity(conn, %{"uuid" => uuid}) do def activity(conn, %{"uuid" => uuid}) do
with id <- o_status_url(conn, :activity, uuid), with id <- o_status_url(conn, :activity, uuid),
%Activity{} = activity <- Activity.get_by_ap_id(id), %Activity{} = activity <- Activity.get_by_ap_id(id),
@ -92,6 +91,7 @@ def activity(conn, %{"uuid" => uuid}) do
end end
end end
# TODO: Data leak
def notice(conn, %{"id" => id}) do def notice(conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id), with %Activity{} = activity <- Repo.get(Activity, id),
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do

View File

@ -12,6 +12,12 @@ def to_simple_form(user) do
[] []
end end
ap_enabled = if user.local do
[{:ap_enabled, ['true']}]
else
[]
end
[ [
{:id, [ap_id]}, {:id, [ap_id]},
{:"activity:object", ['http://activitystrea.ms/schema/1.0/person']}, {:"activity:object", ['http://activitystrea.ms/schema/1.0/person']},
@ -22,6 +28,6 @@ def to_simple_form(user) do
{:summary, [bio]}, {:summary, [bio]},
{:name, [nickname]}, {:name, [nickname]},
{:link, [rel: 'avatar', href: avatar_url], []} {:link, [rel: 'avatar', href: avatar_url], []}
] ++ banner ] ++ banner ++ ap_enabled
end end
end end

View File

@ -222,7 +222,7 @@ def user_fetcher(username) do
end end
pipeline :ostatus do pipeline :ostatus do
plug :accepts, ["xml", "atom", "html"] plug :accepts, ["xml", "atom", "html", "activity+json"]
end end
scope "/", Pleroma.Web do scope "/", Pleroma.Web do
@ -243,7 +243,18 @@ def user_fetcher(username) do
end end
pipeline :activitypub do
plug :accepts, ["activity+json"]
plug Pleroma.Web.Plugs.HTTPSignaturePlug
end
if @federating do if @federating do
scope "/", Pleroma.Web.ActivityPub do
pipe_through :activitypub
post "/users/:nickname/inbox", ActivityPubController, :inbox
post "/inbox", ActivityPubController, :inbox
end
scope "/.well-known", Pleroma.Web do scope "/.well-known", Pleroma.Web do
pipe_through :well_known pipe_through :well_known

View File

@ -29,7 +29,8 @@ def fetch_magic_key(salmon) do
with [data, _, _, _, _] <- decode(salmon), with [data, _, _, _, _] <- decode(salmon),
doc <- XML.parse_document(data), doc <- XML.parse_document(data),
uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc), uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc),
{:ok, %{info: %{"magic_key" => magic_key}}} <- Pleroma.Web.OStatus.find_or_make_user(uri) do {:ok, public_key} <- User.get_public_key_for_ap_id(uri),
magic_key <- encode_key(public_key) do
{:ok, magic_key} {:ok, magic_key}
end end
end end
@ -138,7 +139,8 @@ def encode(private_key, doc) do
{:ok, salmon} {:ok, salmon}
end end
def remote_users(%{data: %{"to" => to}}) do def remote_users(%{data: %{"to" => to} = data}) do
to = to ++ (data["cc"] || [])
to to
|> Enum.map(fn(id) -> User.get_cached_by_ap_id(id) end) |> Enum.map(fn(id) -> User.get_cached_by_ap_id(id) end)
|> Enum.filter(fn(user) -> user && !user.local end) |> Enum.filter(fn(user) -> user && !user.local end)
@ -154,8 +156,16 @@ defp send_to_user(%{info: %{"salmon" => salmon}}, feed, poster) do
defp send_to_user(_,_,_), do: nil defp send_to_user(_,_,_), do: nil
@supported_activities [
"Create",
"Follow",
"Like",
"Announce",
"Undo",
"Delete"
]
def publish(user, activity, poster \\ &@httpoison.post/4) def publish(user, activity, poster \\ &@httpoison.post/4)
def publish(%{info: %{"keys" => keys}} = user, activity, poster) do def publish(%{info: %{"keys" => keys}} = user, %{data: %{"type" => type}} = activity, poster) when type in @supported_activities do
feed = ActivityRepresenter.to_simple_form(activity, user, true) feed = ActivityRepresenter.to_simple_form(activity, user, true)
|> ActivityRepresenter.wrap_with_entry |> ActivityRepresenter.wrap_with_entry
|> :xmerl.export_simple(:xmerl_xml) |> :xmerl.export_simple(:xmerl_xml)

View File

@ -74,7 +74,6 @@ def handle_cast(%{action: :add, topic: topic, socket: socket}, sockets) do
sockets_for_topic = Enum.uniq([socket | sockets_for_topic]) sockets_for_topic = Enum.uniq([socket | sockets_for_topic])
sockets = Map.put(sockets, topic, sockets_for_topic) sockets = Map.put(sockets, topic, sockets_for_topic)
Logger.debug("Got new conn for #{topic}") Logger.debug("Got new conn for #{topic}")
IO.inspect(sockets)
{:noreply, sockets} {:noreply, sockets}
end end
@ -84,12 +83,11 @@ def handle_cast(%{action: :remove, topic: topic, socket: socket}, sockets) do
sockets_for_topic = List.delete(sockets_for_topic, socket) sockets_for_topic = List.delete(sockets_for_topic, socket)
sockets = Map.put(sockets, topic, sockets_for_topic) sockets = Map.put(sockets, topic, sockets_for_topic)
Logger.debug("Removed conn for #{topic}") Logger.debug("Removed conn for #{topic}")
IO.inspect(sockets)
{:noreply, sockets} {:noreply, sockets}
end end
def handle_cast(m, state) do def handle_cast(m, state) do
IO.inspect("Unknown: #{inspect(m)}, #{inspect(state)}") Logger.info("Unknown: #{inspect(m)}, #{inspect(state)}")
{:noreply, state} {:noreply, state}
end end

View File

@ -56,7 +56,8 @@ def to_map(%Activity{data: %{"type" => "Like", "published" => created_at}} = act
} }
end end
def to_map(%Activity{data: %{"type" => "Follow", "published" => created_at, "object" => followed_id}} = activity, %{user: user} = opts) do def to_map(%Activity{data: %{"type" => "Follow", "object" => followed_id}} = activity, %{user: user} = opts) do
created_at = activity.data["published"] || (DateTime.to_iso8601(activity.inserted_at))
created_at = created_at |> Utils.date_to_asctime created_at = created_at |> Utils.date_to_asctime
followed = User.get_cached_by_ap_id(followed_id) followed = User.get_cached_by_ap_id(followed_id)
@ -125,7 +126,7 @@ def to_map(%Activity{data: %{"object" => %{"content" => content} = object}} = ac
mentions = opts[:mentioned] || [] mentions = opts[:mentioned] || []
attentions = activity.data["to"] attentions = activity.recipients
|> Enum.map(fn (ap_id) -> Enum.find(mentions, fn(user) -> ap_id == user.ap_id end) end) |> Enum.map(fn (ap_id) -> Enum.find(mentions, fn(user) -> ap_id == user.ap_id end) end)
|> Enum.filter(&(&1)) |> Enum.filter(&(&1))
|> Enum.map(fn (user) -> UserView.render("show.json", %{user: user, for: opts[:for]}) end) |> Enum.map(fn (user) -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
@ -133,7 +134,9 @@ def to_map(%Activity{data: %{"object" => %{"content" => content} = object}} = ac
conversation_id = conversation_id(activity) conversation_id = conversation_id(activity)
tags = activity.data["object"]["tag"] || [] tags = activity.data["object"]["tag"] || []
possibly_sensitive = Enum.member?(tags, "nsfw") possibly_sensitive = activity.data["object"]["sensitive"] || Enum.member?(tags, "nsfw")
tags = if possibly_sensitive, do: Enum.uniq(["nsfw" | tags]), else: tags
summary = activity.data["object"]["summary"] summary = activity.data["object"]["summary"]
content = if !!summary and summary != "" do content = if !!summary and summary != "" do
@ -161,7 +164,7 @@ def to_map(%Activity{data: %{"object" => %{"content" => content} = object}} = ac
"repeat_num" => announcement_count, "repeat_num" => announcement_count,
"favorited" => to_boolean(favorited), "favorited" => to_boolean(favorited),
"repeated" => to_boolean(repeated), "repeated" => to_boolean(repeated),
"external_url" => object["external_url"], "external_url" => object["external_url"] || object["id"],
"tags" => tags, "tags" => tags,
"activity_type" => "post", "activity_type" => "post",
"possibly_sensitive" => possibly_sensitive "possibly_sensitive" => possibly_sensitive

View File

@ -2,9 +2,8 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter do
use Pleroma.Web.TwitterAPI.Representers.BaseRepresenter use Pleroma.Web.TwitterAPI.Representers.BaseRepresenter
alias Pleroma.Object alias Pleroma.Object
def to_map(%Object{} = object, _opts) do def to_map(%Object{data: %{"url" => [url | _]}} = object, _opts) do
data = object.data data = object.data
url = List.first(data["url"])
%{ %{
url: url["href"] |> Pleroma.Web.MediaProxy.url(), url: url["href"] |> Pleroma.Web.MediaProxy.url(),
mimetype: url["mediaType"], mimetype: url["mediaType"],
@ -13,6 +12,19 @@ def to_map(%Object{} = object, _opts) do
} }
end end
def to_map(%Object{data: %{"url" => url} = data}, _opts) when is_binary(url) do
%{
url: url |> Pleroma.Web.MediaProxy.url(),
mimetype: data["mediaType"],
id: data["uuid"],
oembed: false
}
end
def to_map(%Object{}, _opts) do
%{}
end
# If we only get the naked data, wrap in an object # If we only get the naked data, wrap in an object
def to_map(%{} = data, opts) do def to_map(%{} = data, opts) do
to_map(%Object{data: data}, opts) to_map(%Object{data: data}, opts)

View File

@ -13,26 +13,38 @@ def create_status(%User{} = user, %{"status" => _} = data) do
end end
def fetch_friend_statuses(user, opts \\ %{}) do def fetch_friend_statuses(user, opts \\ %{}) do
opts = Map.put(opts, "blocking_user", user) opts = opts
|> Map.put("blocking_user", user)
|> Map.put("user", user)
|> Map.put("type", ["Create", "Announce", "Follow", "Like"])
ActivityPub.fetch_activities([user.ap_id | user.following], opts) ActivityPub.fetch_activities([user.ap_id | user.following], opts)
|> activities_to_statuses(%{for: user}) |> activities_to_statuses(%{for: user})
end end
def fetch_public_statuses(user, opts \\ %{}) do def fetch_public_statuses(user, opts \\ %{}) do
opts = Map.put(opts, "local_only", true) opts = opts
opts = Map.put(opts, "blocking_user", user) |> Map.put("local_only", true)
|> Map.put("blocking_user", user)
|> Map.put("type", ["Create", "Announce", "Follow"])
ActivityPub.fetch_public_activities(opts) ActivityPub.fetch_public_activities(opts)
|> activities_to_statuses(%{for: user}) |> activities_to_statuses(%{for: user})
end end
def fetch_public_and_external_statuses(user, opts \\ %{}) do def fetch_public_and_external_statuses(user, opts \\ %{}) do
opts = Map.put(opts, "blocking_user", user) opts = opts
|> Map.put("blocking_user", user)
|> Map.put("type", ["Create", "Announce", "Follow"])
ActivityPub.fetch_public_activities(opts) ActivityPub.fetch_public_activities(opts)
|> activities_to_statuses(%{for: user}) |> activities_to_statuses(%{for: user})
end end
def fetch_user_statuses(user, opts \\ %{}) do def fetch_user_statuses(user, opts \\ %{}) do
ActivityPub.fetch_activities([], opts) opts = opts
|> Map.put("type", ["Create"])
ActivityPub.fetch_public_activities(opts)
|> activities_to_statuses(%{for: user}) |> activities_to_statuses(%{for: user})
end end
@ -43,7 +55,7 @@ def fetch_mentions(user, opts \\ %{}) do
def fetch_conversation(user, id) do def fetch_conversation(user, id) do
with context when is_binary(context) <- conversation_id_to_context(id), with context when is_binary(context) <- conversation_id_to_context(id),
activities <- ActivityPub.fetch_activities_for_context(context, %{"blocking_user" => user}), activities <- ActivityPub.fetch_activities_for_context(context, %{"blocking_user" => user, "user" => user}),
statuses <- activities |> activities_to_statuses(%{for: user}) statuses <- activities |> activities_to_statuses(%{for: user})
do do
statuses statuses
@ -53,7 +65,8 @@ def fetch_conversation(user, id) do
end end
def fetch_status(user, id) do def fetch_status(user, id) do
with %Activity{} = activity <- Repo.get(Activity, id) do with %Activity{} = activity <- Repo.get(Activity, id),
true <- ActivityPub.visible_for_user?(activity, user) do
activity_to_status(activity, %{for: user}) activity_to_status(activity, %{for: user})
end end
end end
@ -276,7 +289,7 @@ defp activity_to_status(activity, opts) do
actor = get_in(activity.data, ["actor"]) actor = get_in(activity.data, ["actor"])
user = User.get_cached_by_ap_id(actor) user = User.get_cached_by_ap_id(actor)
# mentioned_users = Repo.all(from user in User, where: user.ap_id in ^activity.data["to"]) # mentioned_users = Repo.all(from user in User, where: user.ap_id in ^activity.data["to"])
mentioned_users = Enum.map(activity.data["to"] || [], fn (ap_id) -> mentioned_users = Enum.map(activity.recipients || [], fn (ap_id) ->
if ap_id do if ap_id do
User.get_cached_by_ap_id(ap_id) User.get_cached_by_ap_id(ap_id)
else else

View File

@ -207,7 +207,8 @@ def register(conn, params) do
def update_avatar(%{assigns: %{user: user}} = conn, params) do def update_avatar(%{assigns: %{user: user}} = conn, params) do
{:ok, object} = ActivityPub.upload(params) {:ok, object} = ActivityPub.upload(params)
change = Changeset.change(user, %{avatar: object.data}) change = Changeset.change(user, %{avatar: object.data})
{:ok, user} = Repo.update(change) {:ok, user} = User.update_and_set_cache(change)
CommonAPI.update(user)
render(conn, UserView, "show.json", %{user: user, for: user}) render(conn, UserView, "show.json", %{user: user, for: user})
end end
@ -216,7 +217,8 @@ def update_banner(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}), with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}),
new_info <- Map.put(user.info, "banner", object.data), new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}), change <- User.info_changeset(user, %{info: new_info}),
{:ok, _user} <- Repo.update(change) do {:ok, user} <- User.update_and_set_cache(change) do
CommonAPI.update(user)
%{"url" => [ %{ "href" => href } | _ ]} = object.data %{"url" => [ %{ "href" => href } | _ ]} = object.data
response = %{ url: href } |> Poison.encode! response = %{ url: href } |> Poison.encode!
conn conn
@ -228,7 +230,7 @@ def update_background(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(params), with {:ok, object} <- ActivityPub.upload(params),
new_info <- Map.put(user.info, "background", object.data), new_info <- Map.put(user.info, "background", object.data),
change <- User.info_changeset(user, %{info: new_info}), change <- User.info_changeset(user, %{info: new_info}),
{:ok, _user} <- Repo.update(change) do {:ok, _user} <- User.update_and_set_cache(change) do
%{"url" => [ %{ "href" => href } | _ ]} = object.data %{"url" => [ %{ "href" => href } | _ ]} = object.data
response = %{ url: href } |> Poison.encode! response = %{ url: href } |> Poison.encode!
conn conn
@ -255,7 +257,7 @@ def update_most_recent_notification(%{assigns: %{user: user}} = conn, %{"id" =>
mrn <- max(id, user.info["most_recent_notification"] || 0), mrn <- max(id, user.info["most_recent_notification"] || 0),
updated_info <- Map.put(info, "most_recent_notification", mrn), updated_info <- Map.put(info, "most_recent_notification", mrn),
changeset <- User.info_changeset(user, %{info: updated_info}), changeset <- User.info_changeset(user, %{info: updated_info}),
{:ok, _user} <- Repo.update(changeset) do {:ok, _user} <- User.update_and_set_cache(changeset) do
conn conn
|> json_reply(200, Poison.encode!(mrn)) |> json_reply(200, Poison.encode!(mrn))
else else
@ -305,7 +307,8 @@ def update_profile(%{assigns: %{user: user}} = conn, params) do
end end
with changeset <- User.update_changeset(user, params), with changeset <- User.update_changeset(user, params),
{:ok, user} <- Repo.update(changeset) do {:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user)
render(conn, UserView, "user.json", %{user: user, for: user}) render(conn, UserView, "user.json", %{user: user, for: user})
else else
error -> error ->

View File

@ -45,6 +45,7 @@ def represent_user(user) do
{:Link, %{rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: user.ap_id}}, {:Link, %{rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: user.ap_id}},
{:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}}, {:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}},
{:Link, %{rel: "magic-public-key", href: "data:application/magic-public-key,#{magic_key}"}}, {:Link, %{rel: "magic-public-key", href: "data:application/magic-public-key,#{magic_key}"}},
{:Link, %{rel: "self", type: "application/activity+json", href: user.ap_id}},
{:Link, %{rel: "http://ostatus.org/schema/1.0/subscribe", template: OStatus.remote_follow_path()}} {:Link, %{rel: "http://ostatus.org/schema/1.0/subscribe", template: OStatus.remote_follow_path()}}
] ]
} }
@ -59,7 +60,8 @@ def ensure_keys_present(user) do
else else
{:ok, pem} = Salmon.generate_rsa_pem {:ok, pem} = Salmon.generate_rsa_pem
info = Map.put(info, "keys", pem) info = Map.put(info, "keys", pem)
Repo.update(Ecto.Changeset.change(user, info: info)) Ecto.Changeset.change(user, info: info)
|> User.update_and_set_cache()
end end
end end
@ -70,12 +72,14 @@ defp webfinger_from_xml(doc) do
subject = XML.string_from_xpath("//Subject", doc) subject = XML.string_from_xpath("//Subject", doc)
salmon = XML.string_from_xpath(~s{//Link[@rel="salmon"]/@href}, doc) salmon = XML.string_from_xpath(~s{//Link[@rel="salmon"]/@href}, doc)
subscribe_address = XML.string_from_xpath(~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}, doc) subscribe_address = XML.string_from_xpath(~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}, doc)
ap_id = XML.string_from_xpath(~s{//Link[@rel="self" and @type="application/activity+json"]/@href}, doc)
data = %{ data = %{
"magic_key" => magic_key, "magic_key" => magic_key,
"topic" => topic, "topic" => topic,
"subject" => subject, "subject" => subject,
"salmon" => salmon, "salmon" => salmon,
"subscribe_address" => subscribe_address "subscribe_address" => subscribe_address,
"ap_id" => ap_id
} }
{:ok, data} {:ok, data}
end end
@ -102,6 +106,7 @@ def find_lrdd_template(domain) do
end end
def finger(account) do def finger(account) do
account = String.trim_leading(account, "@")
domain = with [_name, domain] <- String.split(account, "@") do domain = with [_name, domain] <- String.split(account, "@") do
domain domain
else _e -> else _e ->

View File

@ -38,7 +38,15 @@ def verify(subscription, getter \\ &@httpoison.get/3) do
end end
end end
def publish(topic, user, activity) do @supported_activities [
"Create",
"Follow",
"Like",
"Announce",
"Undo",
"Delete"
]
def publish(topic, user, %{data: %{"type" => type}} = activity) when type in @supported_activities do
# TODO: Only send to still valid subscriptions. # TODO: Only send to still valid subscriptions.
query = from sub in WebsubServerSubscription, query = from sub in WebsubServerSubscription,
where: sub.topic == ^topic and sub.state == "active" where: sub.topic == ^topic and sub.state == "active"
@ -58,6 +66,7 @@ def publish(topic, user, activity) do
Pleroma.Web.Federator.enqueue(:publish_single_websub, data) Pleroma.Web.Federator.enqueue(:publish_single_websub, data)
end) end)
end end
def publish(_,_,_), do: ""
def sign(secret, doc) do def sign(secret, doc) do
:crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16 |> String.downcase :crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16 |> String.downcase

View File

@ -0,0 +1,11 @@
defmodule Pleroma.Repo.Migrations.AddRecipientsToActivities do
use Ecto.Migration
def change do
alter table(:activities) do
add :recipients, {:array, :string}
end
create index(:activities, [:recipients], using: :gin)
end
end

View File

@ -0,0 +1,21 @@
defmodule Pleroma.Repo.Migrations.FillRecipientsInActivities do
use Ecto.Migration
alias Pleroma.{Repo, Activity}
def up do
max = Repo.aggregate(Activity, :max, :id)
if max do
IO.puts("#{max} activities")
chunks = 0..(round(max / 10_000))
Enum.each(chunks, fn (i) ->
min = i * 10_000
max = min + 10_000
execute("""
update activities set recipients = array(select jsonb_array_elements_text(data->'to')) where id > #{min} and id <= #{max};
""")
|> IO.inspect
end)
end
end
end

View File

@ -0,0 +1,18 @@
defmodule Pleroma.Repo.Migrations.MakeFollowingPostgresArray do
use Ecto.Migration
def change do
alter table(:users) do
add :following_temp, {:array, :string}
end
execute """
update users set following_temp = array(select jsonb_array_elements_text(following));
"""
alter table(:users) do
remove :following
end
rename table(:users), :following_temp, to: :following
end
end

1
test/fixtures/avatar_data_uri vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:georss="http://www.georss.org/georss" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:statusnet="http://status.net/schema/api/1/">
<activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
<id>tag:shitposter.club,2018-02-22:noticeId=7369654:objectType=comment</id>
<title>New comment by shpuld</title>
<content type="html">@&lt;a href=&quot;https://testing.pleroma.lol/users/lain&quot; class=&quot;h-card mention&quot; title=&quot;Rael Electric Razor&quot;&gt;lain&lt;/a&gt; me far right</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7369654"/>
<status_net notice_id="7369654"></status_net>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<published>2018-02-22T09:20:12+00:00</published>
<updated>2018-02-22T09:20:12+00:00</updated>
<author>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<uri>https://shitposter.club/user/5381</uri>
<name>shpuld</name>
<link rel="alternate" type="text/html" href="https://shitposter.club/shpuld"/>
<link rel="avatar" type="image/png" media:width="864" media:height="864" href="https://shitposter.club/avatar/5381-original-20171230093854.png"/>
<link rel="avatar" type="image/png" media:width="96" media:height="96" href="https://shitposter.club/avatar/5381-96-20171230093854.png"/>
<link rel="avatar" type="image/png" media:width="48" media:height="48" href="https://shitposter.club/avatar/5381-48-20171230093854.png"/>
<link rel="avatar" type="image/png" media:width="24" media:height="24" href="https://shitposter.club/avatar/5381-24-20171230093900.png"/>
<poco:preferredUsername>shpuld</poco:preferredUsername>
<poco:displayName>shp</poco:displayName>
<followers url="https://shitposter.club/shpuld/subscribers"></followers>
<statusnet:profile_info local_id="5381"></statusnet:profile_info>
</author>
<thr:in-reply-to ref="https://testing.pleroma.lol/objects/b319022a-4946-44c5-9de9-34801f95507b" href="https://testing.pleroma.lol/objects/b319022a-4946-44c5-9de9-34801f95507b"></thr:in-reply-to>
<link rel="related" href="https://testing.pleroma.lol/objects/b319022a-4946-44c5-9de9-34801f95507b"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4378601"/>
<ostatus:conversation href="https://shitposter.club/conversation/4378601" local_id="4378601" ref="tag:shitposter.club,2018-02-22:objectType=thread:nonce=e5a7c72d60a9c0e4">tag:shitposter.club,2018-02-22:objectType=thread:nonce=e5a7c72d60a9c0e4</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://testing.pleroma.lol/users/lain"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<source>
<id>https://shitposter.club/api/statuses/user_timeline/5381.atom</id>
<title>shp</title>
<link rel="alternate" type="text/html" href="https://shitposter.club/shpuld"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/user_timeline/5381.atom"/>
<link rel="license" href="https://shitposter.club/doc/tos"/>
<icon>https://shitposter.club/avatar/5381-96-20171230093854.png</icon>
<updated>2018-02-23T13:30:15+00:00</updated>
</source>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7369654.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7369654.atom"/>
<statusnet:notice_info local_id="7369654" source="Pleroma FE"></statusnet:notice_info>
</entry>

View File

@ -0,0 +1,665 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Shitposter Club</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0">
<link rel="stylesheet" type="text/css" href="https://shitposter.club/plugins/Qvitter/css/qvitter.css?changed=20170610161937" />
<link rel="stylesheet" type="text/css" href="https://shitposter.club/plugins/Qvitter/css/jquery.minicolors.css" />
<link rel="apple-touch-icon" sizes="57x57" href="https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/apple-touch-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/apple-touch-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/apple-touch-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/apple-touch-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/apple-touch-icon-180x180.png">
<link rel="icon" type="image/png" href="https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png" sizes="16x16">
<link rel="icon" type="image/png" href="https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/android-chrome-192x192.png" sizes="192x192">
<link rel="icon" type="image/png" href="https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-96x96.png" sizes="96x96">
<link rel="manifest" href="https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/manifest.json">
<link rel="mask-icon" href="https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/safari-pinned-tab.svg" color="#a22430">
<meta name="apple-mobile-web-app-title" content="Shitposter Club">
<meta name="application-name" content="Shitposter Club">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-TileImage" content="https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/mstile-144x144.png">
<meta name="theme-color" content="#ffffff">
<link title="oEmbed" href="https://shitposter.club/services/oembed.json?url=https%3A%2F%2Fshitposter.club%2Fnotice%2F7369654" type="application/json+oembed" rel="alternate">
<link title="oEmbed" href="https://shitposter.club/services/oembed.xml?url=https%3A%2F%2Fshitposter.club%2Fnotice%2F7369654" type="application/xml+oembed" rel="alternate">
<link title="Single notice (JSON)" href="https://shitposter.club/api/statuses/show/7369654.json" type="application/stream+json" rel="alternate">
<link title="Single notice (Atom)" href="https://shitposter.club/api/statuses/show/7369654.atom" type="application/atom+xml" rel="alternate">
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="shp (@shpuld)" />
<meta name="twitter:description" content="@lain me far right" />
<meta property="og:description" content="@lain me far right" />
<meta property="og:site_name" content="Shitposter Club" />
<script>
/*
@licstart The following is the entire license notice for the
JavaScript code in this page.
Copyright (C) 2015 Hannes Mannerheim and other contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
@licend The above is the entire license notice
for the JavaScript code in this page.
*/
window.usersLanguageCode = "en";
window.usersLanguageNameInEnglish = "English";
window.englishLanguageData = {
"directionality":"ltr",
"languageName": "English",
"loginUsername": "Username or e-mail",
"loginPassword": "Password",
"loginSignIn": "Sign in",
"loginRememberMe": "Remember me",
"loginForgotPassword": "Forgot password?",
"notices": "Notices",
"followers": "Followers",
"following": "Following",
"groups": "Groups",
"compose": "PULL THE TRIGGER",
"queetVerb": "Send",
"queetsNounPlural": "Notices",
"logout": "Sign out",
"languageSelected": "Language:",
"viewMyProfilePage": "View my profile page",
"expand": "Expand",
"collapse": "Collapse",
"details": "Details",
"expandFullConversation": "Expand full conversation",
"replyVerb": "Reply",
"requeetVerb": "Repeat",
"favoriteVerb": "Favorite",
"requeetedVerb": "Repeated",
"favoritedVerb": "Favorited",
"replyTo": "Reply to",
"requeetedBy": "Repeated by {requeeted-by}",
"favoriteNoun": "Favorite",
"favoritesNoun": "Favorites",
"requeetNoun": "Repeat",
"requeetsNoun": "Repeats",
"newQueet": "{new-notice-count} new notice",
"newQueets": "{new-notice-count} new notices",
"longmonthsJanuary": "January",
"longmonthsFebruary": "February",
"longmonthsMars": "March",
"longmonthsApril": "April",
"longmonthsMay": "May",
"longmonthsJune": "June",
"longmonthsJuly": "July",
"longmonthsAugust": "August",
"longmonthsSeptember": "September",
"longmonthsOctober": "October",
"longmonthsNovember": "November",
"longmonthsDecember": "December",
"shortmonthsJanuary": "jan",
"shortmonthsFebruary": "feb",
"shortmonthsMars": "mar",
"shortmonthsApril": "apr",
"shortmonthsMay": "may",
"shortmonthsJune": "jun",
"shortmonthsJuly": "jul",
"shortmonthsAugust": "aug",
"shortmonthsSeptember": "sep",
"shortmonthsOctober": "oct",
"shortmonthsNovember": "nov",
"shortmonthsDecember": "dec",
"time12am": "{time} am",
"time12pm": "{time} pm",
"longDateFormat": "{time12} - {day} {month} {year}",
"shortDateFormatSeconds": "{seconds}s",
"shortDateFormatMinutes": "{minutes}m",
"shortDateFormatHours": "{hours}h",
"shortDateFormatDate": "{day} {month}",
"shortDateFormatDateAndY": "{day} {month} {year}",
"now": "now",
"posting": "posting",
"viewMoreInConvBefore": "← View more in conversation",
"viewMoreInConvAfter": "View more in conversation →",
"mentions": "Mentions",
"timeline": "Only Who I'm Following",
"publicTimeline": "Everyone on Shitposter Club",
"publicAndExtTimeline": "MY EYES! I CAN SEE FOREVER",
"searchVerb": "Search",
"deleteVerb": "Delete",
"cancelVerb": "Cancel",
"deleteConfirmation": "Are you sure you want to delete this notice?",
"userExternalFollow": "Remote follow",
"userExternalFollowHelp": "Your account ID (e.g. user@rainbowdash.net).",
"userFollow": "Follow",
"userFollowing": "Following",
"userUnfollow": "Unfollow",
"joinGroup": "Join",
"joinExternalGroup": "Join remotely",
"isMemberOfGroup": "Member",
"leaveGroup": "Leave",
"memberCount": "Members",
"adminCount": "Admins",
"settings": "Settings",
"saveChanges": "Save changes",
"linkColor": "Link color",
"backgroundColor": "Background color",
"newToQuitter": "New to {site-title}?",
"signUp": "Sign up",
"signUpFullName": "Full name",
"signUpEmail": "Email",
"signUpButtonText": "Sign up to {site-title}",
"welcomeHeading": "Welcome to {site-title}.",
"welcomeText": "We are a <span id=\"federated-tooltip\"><div id=\"what-is-federation\">\"Federation\" means that you don't need a {site-title} account to be able to follow, be followed by or interact with {site-title} users. You can register on any StatusNet or GNU social server or any service based on the the <a href=\"http://www.w3.org/community/ostatus/wiki/Main_Page\">Ostatus</a> protocol! You don't even have to join a service try installing the lovely <a href=\"http://www.gnu.org/software/social/\">GNU social</a> software on your own server! :)</div>federation</span> of microbloggers who care about social justice and solidarity and want to quit the centralised capitalist services.",
"registerNickname": "Nickname",
"registerHomepage": "Homepage",
"registerBio": "Bio",
"registerLocation": "Location",
"registerRepeatPassword": "Repeat password",
"moreSettings": "More settings",
"otherServers": "Alternatively you can create an account on another server of the GNU social network. <a href=\"http://federation.skilledtests.com/select_your_server.html\">Comparison</a>",
"editMyProfile": "Edit profile",
"notifications": "Notifications",
"xFavedYourQueet": "favorited your notice",
"xRepeatedYourQueet": "repeated you",
"xStartedFollowingYou": "followed you",
"followsYou": "follows you",
"FAQ": "FAQ",
"inviteAFriend": "Invite a friend!",
"goToExternalProfile": "Go to full profile",
"cropAndSave": "Crop and save",
"showTerms": "Read our Terms of Use",
"ellipsisMore": "More",
"blockUser": "Block",
"goToOriginalNotice": "Go to the original notice",
"goToTheUsersRemoteProfile": "Go to the user's remote profile",
"clickToDrag":"Click to drag",
"keyboardShortcuts":"Keyboard shortcuts",
"classicInterface":"Classic {site-title}",
"accessibilityToggleLink":"For better accessibility, click this link to switch to the classic interface",
"tooltipBookmarkStream":"Add this stream to your bookmarks",
"tooltipTopMenu":"Menu and settings",
"tooltipAttachImage":"Attach an image",
"tooltipShortenUrls":"Shorten all URLs in the notice",
"tooltipReloadStream":"Refresh this stream",
"tooltipRemoveBookmark":"Remove this bookmark",
"clearHistory":"Clear browsing history",
"ERRORsomethingWentWrong":"Something went wrong.",
"ERRORmustBeLoggedIn":"You must be logged in to view this stream.",
"ERRORcouldNotFindUserWithNickname":"Could not find a user with nickname \"{nickname}\" on this server",
"ERRORcouldNotFindGroupWithNickname":"Could not find a group with nickname \"{nickname}\" on this server",
"ERRORcouldNotFindPage":"Could not find that page.",
"ERRORnoticeRemoved": "This notice has been removed.",
"ERRORnoContactWithServer": "Can not establish a connection to the server. The server could be overloaded, or there might be a problem with your internet connection. Please try again later!",
"ERRORattachmentUploadFailed": "The upload failed. The format might be unsupported or the size too large.",
"hideRepliesToPeopleIDoNotFollow":"Hide replies to people I don't follow",
"markAllNotificationsAsSeen":"Mark all notifications as seen",
"notifyRepliesAndMentions":"Mentions and replies",
"notifyFavs":"Favorites",
"notifyRepeats":"Repeats",
"notifyFollows":"New followers",
"timelineOptions":"Timeline options",
"ERRORfailedSavingYourSetting":"Failed saving your setting",
"ERRORfailedMarkingAllNotificationsAsRead":"Failed marking all notifications as seen.",
"newNotification": "{new-notice-count} new notification",
"newNotifications": "{new-notice-count} new notifications",
"thisIsANoticeFromABlockedUser":"Warning: This is a quip from a user you have blocked. Click to show it.",
"nicknamesListWithListName":"{nickname}s list: {list-name}",
"myListWithListName":"My list: {list-name}",
"listMembers":"Members",
"listSubscribers":"Subscribers",
"ERRORcouldNotFindList":"There is no such list.",
"emailAlreadyInUse":"Already in use",
"addEditLanguageLink":"Help translate {site-title} to another language",
"onlyPartlyTranslated":"{site-title} is only partly translated to <em>{language-name}</em> ({percent}%). You can help complete the translation at <a href=\"https://git.gnu.io/h2p/Qvitter/tree/master/locale\">Qvitter's repository homepage</a>",
"startRant":"Start a rant",
"continueRant":"Continue the rant",
"hideEmbeddedInTimeline":"Hide embedded content in this timeline",
"hideQuotesInTimeline":"Hide quotes in this timeline",
"userBlocks":"Accounts you're blocking",
"buttonBlocked":"Blocked",
"buttonUnblock":"Unblock",
"failedBlockingUser":"Failed to block the user.",
"failedUnblockingUser":"Failed to unblock the user.",
"unblockUser": "Unblock",
"tooltipBlocksYou":"You are blocked from following {username}.",
"silenced":"Silenced",
"silencedPlural":"Silenced profiles",
"silencedUsersOnThisInstance":"Silenced profiles on {site-title}",
"sandboxed":"Sandboxed",
"sandboxedPlural":"Sandboxed profiles",
"sandboxedUsersOnThisInstance":"Sandboxed profiles on {site-title}",
"silencedStreamDescription":"Silenced users can't login or post quips and the quips they've already posted are hidden. For local users it's like a delete that can be reversed, for remote users it's like a site wide block.",
"sandboxedStreamDescription":"Quips from sandboxed users are excluded from the Public Timeline and The Whole Known Network. Apart from that, they can use the site like any other user.",
"onlyShowNotificationsFromUsersIFollow":"Only show notifications from users I follow",
"userOptions":"More user actions",
"silenceThisUser":"Silence {nickname}",
"sandboxThisUser":"Sandbox {nickname}",
"unSilenceThisUser":"Unsilence {nickname}",
"unSandboxThisUser":"Unsandbox {nickname}",
"ERRORfailedSandboxingUser":"Failed sandboxing/unsandboxing the user",
"ERRORfailedSilencingUser":"Failed silencing/unsilencing the user",
"muteUser":"Mute",
"unmuteUser":"Unmute",
"hideNotificationsFromMutedUsers":"Hide notifications from muted users",
"thisIsANoticeFromAMutedUser":"You have muted the author of this quip. Click here to show it anyway.",
"userMutes":"Accounts you're muting",
"userBlocked":"Blocked accounts",
"userMuted":"Muted accounts",
"mutedStreamDescription":"You've hidden these accounts from your timeline. You will still receive notifications from these accounts, unless you select &quot;Hide notifications from muted users&quot; from the cog wheel menu on the notifications page.",
"profileAndSettings":"Profile and settings",
"profileSettings":"Profile settings",
"thisIsABookmark":"This is a bookmark created in the Classic interface",
"thisIsARemoteUser":"<strong>Attention!</strong> This is a remote user. This page is only a cached copy of their profile, and includes only data known to this GNU social instance. Go to the <a href=\"{remote-profile-url}\" donthijack>user's profile on their server</a> to view their full profile.",
"findSomeone":"Find someone",
"findSomeoneTooltip":"Input a username or a profile url, e.g. @localuser or https://remote.instance/nickname",
"tooltipAttachFile":"Attach a file"
}
;
window.defaultAvatarStreamSize = "https:\/\/shitposter.club\/theme\/neo-gnu\/default-avatar-stream.png";
window.defaultAvatarProfileSize = "https:\/\/shitposter.club\/theme\/neo-gnu\/default-avatar-profile.png";
window.textLimit = 3800;
window.registrationsClosed = false;
window.thisSiteThinksItIsHttpButIsActuallyHttps = false;
window.siteTitle = "Shitposter Club";
window.loggedIn = false;
window.timeBetweenPolling = 5000;
window.apiRoot = 'https://shitposter.club/api/';
window.fullUrlToThisQvitterApp = 'https://shitposter.club/plugins/Qvitter/';
window.siteRootDomain = 'shitposter.club';
window.siteInstanceURL = 'https://shitposter.club/';
window.avatarServer= "";
window.defaultLinkColor = '#0084B4';
window.defaultBackgroundColor = '#f4f4f4';
window.siteBackground = '../../file/cityscape.jpg';
window.enableWelcomeText = true;
window.customWelcomeText = {"en":"<h1 style=\"text-align: center;\"><img src=\"\/custom\/spclublogo-05.png\" alt=\"Shitposter Club\"><br>A safe space on the Internet<\/h1>"};
window.urlShortenerAPIURL = 'http://qttr.at/yourls-api.php';
window.urlShortenerSignature = 'b6afeec983';
window.urlshortenerFormat = 'jsonp';
window.commonSessionToken = '99dbb9040190c2c0d1e0a991204b088116ba434cfcf532c2d423fdbd67647d1a2b737b446dfd81579980e6acd53ad37974801547b69f293e008f45bd5b89bc4a';
window.siteMaxThumbnailSize = 1000;
window.siteAttachmentURLBase = 'https://shitposter.club//file/';
window.siteAvatarURLBase = 'https://shitposter.club//avatar/';
window.siteEmail = 'shitposterclub@gmail.com';
window.siteLicenseTitle = '';
window.siteLicenseURL = 'https://shitposter.club/doc/tos';
window.customTermsOfUse = "<h2>The Rules<\/h2>\n<ol>\n<li>Do not post content that is illegal in the United States of America.<\/li>\n<li>Do not engage in behavior onsite that would get the admin or his hosting\nthreatened, e.g. doxing, harassment, posting copyrighted content that\nwill get the site DMCA'd, etc. This is a vague rule, sorry, it can't be\nhelped.<\/li>\n<li>The site should be considered NOT SAFE FOR WORK (NSFW), <em>however<\/em>,\nwe DO NOT allow: \n <ul>\n <li>\"excessive or extreme pornography\"<\/li>\n <li>gore or \"gross-out\" (e.g. \"tubgirl\") pics<\/li>\n <li>so-called \"loli hentai\" aka sexually explicit drawn depictions of children<\/li>\n <li>\"child model\" pictures<\/li>\n <\/ul>\n ...on the \"public\" (\"everyone on Shitposter Club\") timeline.\n <p>\n What this means is, do not post these pictures, or \"repeat\" them from The Whole Known Network (\"My eyes!\") timeline, or embed them.<\/li>\n<li>Do not engage in behavior that harms the functionality of the site\nitself, e.g. no hacking or exploiting it or spamming. If you're told you're doing\nsomething that is harming the technical operation of the site, stop doing it. The\nadmin's word is final.<\/li>\n<\/ol>\n<h2>My Pledge to You<\/h2>\n<p>I will not ban you or delete your posts for:\nBeing a jerk, having a terrible opinion, disagreeing with me, engaging in so-called \"hate\" or \"offensive\" speech (we have a block button, use it.)<\/p>\n<p>I will ban you or delete your posts for:\nBreaking the rules above, intentionally evading a block to post directly\nat someone who has blocked you, basically antisocial behavior that\ndirectly tries to get around any of the other rules. I will TRY to be lenient and tolerant about rules and not be a ban-Nazi.<\/p>\n<p>You own your posts, but due to the nature of federated services you\nare granting an irrevocable license for others on the network to\nsyndicate it. You are responsible for what you post.<\/p>";
window.siteLocalOnlyDefaultPath = true;
window.disableKeyboardShortcuts = false;
// available language files and their last update time
window.availableLanguages = {
"ar": "ar.json?changed=20170610161937",
"ast": "ast.json?changed=20170610161937",
"ca": "ca.json?changed=20170610161937",
"de": "de.json?changed=20170610161937",
"en": "en.json?changed=20170610161937",
"eo": "eo.json?changed=20170610161937",
"es_419": "es_419.json?changed=20170610161937",
"es": "es.json?changed=20170610161937",
"eu": "eu.json?changed=20170610161937",
"fa": "fa.json?changed=20170610161937",
"fi": "fi.json?changed=20170610161937",
"fr": "fr.json?changed=20170610161937",
"gl": "gl.json?changed=20170610161937",
"he": "he.json?changed=20170610161937",
"hy": "hy.json?changed=20170610161937",
"ia": "ia.json?changed=20170610161937",
"io": "io.json?changed=20170610161937",
"it": "it.json?changed=20170610161937",
"ja": "ja.json?changed=20170610161937",
"nb": "nb.json?changed=20170610161937",
"nl": "nl.json?changed=20170610161937",
"pl": "pl.json?changed=20170610161937",
"pt_br": "pt_br.json?changed=20170610161937",
"pt": "pt.json?changed=20170610161937",
"ru": "ru.json?changed=20170610161937",
"sq": "sq.json?changed=20170610161937",
"sv": "sv.json?changed=20170610161937",
"tr": "tr.json?changed=20170610161937",
"uk": "uk.json?changed=20170610161937",
"zh_cn": "zh_cn.json?changed=20170610161937",
"zh_tw": "zh_tw.json?changed=20170610161937",
};
</script>
<link href='https://shitposter.club/plugins/QvitterSimpleSecurity/css/ss.css?changed=20160925025913' rel='stylesheet' type='text/css'> </head>
<body class="" style="background-color:#f4f4f4">
<input id="upload-image-input" class="upload-image-input" type="file" name="upload-image-input">
<div class="topbar">
<a href="https://shitposter.club/main/public"><div id="logo"></div></a><div id="top-compose" class="hidden"></div>
<ul class="quitter-settings dropdown-menu">
<li class="dropdown-caret right">
<span class="caret-outer"></span>
<span class="caret-inner"></span>
</li>
<li class="fullwidth"><a id="top-menu-profile-link" class="no-hover-card" href="https://shitposter.club/"><div id="top-menu-profile-link-fullname"></div><div id="top-menu-profile-link-view-profile"></div></a></li>
<li class="fullwidth dropdown-divider"></li>
<li class="fullwidth"><a id="faq-link"></a></li>
<li class="fullwidth"><a id="tou-link"></a></li>
<li class="fullwidth"><a id="shortcuts-link"></a></li> <li class="fullwidth"><a id="invite-link" href="https://shitposter.club/main/invite"></a></li>
<li class="fullwidth"><a id="classic-link"></a></li>
<li class="fullwidth dropdown-divider"></li>
<li class="fullwidth"><a id="logout"></a></li>
<li class="fullwidth language dropdown-divider"></li>
<li class="language"><a class="language-link" data-tooltip="العربية Arabic" data-lang-code="ar">العربية</a></li><li class="language"><a class="language-link" data-tooltip="asturianu Asturian" data-lang-code="ast">asturianu</a></li><li class="language"><a class="language-link" data-tooltip="català Catalan" data-lang-code="ca">català</a></li><li class="language"><a class="language-link" data-tooltip="Deutsch German" data-lang-code="de">Deutsch</a></li><li class="language"><a class="language-link" data-tooltip="English" data-lang-code="en">English</a></li><li class="language"><a class="language-link" data-tooltip="esperanto Esperanto" data-lang-code="eo">esperanto</a></li><li class="language"><a class="language-link" data-tooltip="español (Latinoamérica) Spanish (Latin America)" data-lang-code="es_419">español (Latinoamérica)</a></li><li class="language"><a class="language-link" data-tooltip="español Spanish" data-lang-code="es">español</a></li><li class="language"><a class="language-link" data-tooltip="euskara Basque" data-lang-code="eu">euskara</a></li><li class="language"><a class="language-link" data-tooltip="فارسی Persian" data-lang-code="fa">فارسی</a></li><li class="language"><a class="language-link" data-tooltip="suomi Finnish" data-lang-code="fi">suomi</a></li><li class="language"><a class="language-link" data-tooltip="français French" data-lang-code="fr">français</a></li><li class="language"><a class="language-link" data-tooltip="galego Galician" data-lang-code="gl">galego</a></li><li class="language"><a class="language-link" data-tooltip="עברית Hebrew" data-lang-code="he">עברית</a></li><li class="language"><a class="language-link" data-tooltip="հայերեն Armenian" data-lang-code="hy">հայերեն</a></li><li class="language"><a class="language-link" data-tooltip="Interlingua" data-lang-code="ia">Interlingua</a></li><li class="language"><a class="language-link" data-tooltip="Ido" data-lang-code="io">Ido</a></li><li class="language"><a class="language-link" data-tooltip="italiano Italian" data-lang-code="it">italiano</a></li><li class="language"><a class="language-link" data-tooltip="日本語 Japanese" data-lang-code="ja">日本語</a></li><li class="language"><a class="language-link" data-tooltip="norsk bokmål Norwegian Bokmål" data-lang-code="nb">norsk bokmål</a></li><li class="language"><a class="language-link" data-tooltip="Nederlands Dutch" data-lang-code="nl">Nederlands</a></li><li class="language"><a class="language-link" data-tooltip="polski Polish" data-lang-code="pl">polski</a></li><li class="language"><a class="language-link" data-tooltip="português (Brasil) Portuguese (Brazil)" data-lang-code="pt_br">português (Brasil)</a></li><li class="language"><a class="language-link" data-tooltip="português Portuguese" data-lang-code="pt">português</a></li><li class="language"><a class="language-link" data-tooltip="русский Russian" data-lang-code="ru">русский</a></li><li class="language"><a class="language-link" data-tooltip="shqip Albanian" data-lang-code="sq">shqip</a></li><li class="language"><a class="language-link" data-tooltip="svenska Swedish" data-lang-code="sv">svenska</a></li><li class="language"><a class="language-link" data-tooltip="Türkçe Turkish" data-lang-code="tr">Türkçe</a></li><li class="language"><a class="language-link" data-tooltip="українська Ukrainian" data-lang-code="uk">українська</a></li><li class="language"><a class="language-link" data-tooltip="中文(中国) Chinese (China)" data-lang-code="zh_cn">中文(中国)</a></li><li class="language"><a class="language-link" data-tooltip="中文(台灣) Chinese (Taiwan)" data-lang-code="zh_tw">中文(台灣)</a></li> <li class="fullwidth language dropdown-divider"></li>
<li class="fullwidth"><a href="https://git.gnu.io/h2p/Qvitter/tree/master/locale" target="_blank" id="add-edit-language-link"></a></li>
</ul>
<div class="global-nav">
<div class="global-nav-inner">
<div class="container">
<div id="search">
<input type="text" spellcheck="false" autocomplete="off" name="q" placeholder="Sök" id="search-query" class="search-input">
<span class="search-icon">
<button class="icon nav-search" type="submit" tabindex="-1">
<span> Sök </span>
</button>
</span>
</div>
<ul class="language-dropdown">
<li class="dropdown">
<a class="dropdown-toggle">
<small></small>
<span class="current-language"></span>
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li class="dropdown-caret right">
<span class="caret-outer"></span>
<span class="caret-inner"></span>
</li>
<li><a class="language-link" data-tooltip="Arabic" data-lang-code="ar">العربية</a></li><li><a class="language-link" data-tooltip="Asturian" data-lang-code="ast">asturianu</a></li><li><a class="language-link" data-tooltip="Catalan" data-lang-code="ca">català</a></li><li><a class="language-link" data-tooltip="German" data-lang-code="de">Deutsch</a></li><li><a class="language-link" data-tooltip="English" data-lang-code="en">English</a></li><li><a class="language-link" data-tooltip="Esperanto" data-lang-code="eo">esperanto</a></li><li><a class="language-link" data-tooltip="Spanish (Latin America)" data-lang-code="es_419">español (Latinoamérica)</a></li><li><a class="language-link" data-tooltip="Spanish" data-lang-code="es">español</a></li><li><a class="language-link" data-tooltip="Basque" data-lang-code="eu">euskara</a></li><li><a class="language-link" data-tooltip="Persian" data-lang-code="fa">فارسی</a></li><li><a class="language-link" data-tooltip="Finnish" data-lang-code="fi">suomi</a></li><li><a class="language-link" data-tooltip="French" data-lang-code="fr">français</a></li><li><a class="language-link" data-tooltip="Galician" data-lang-code="gl">galego</a></li><li><a class="language-link" data-tooltip="Hebrew" data-lang-code="he">עברית</a></li><li><a class="language-link" data-tooltip="Armenian" data-lang-code="hy">հայերեն</a></li><li><a class="language-link" data-tooltip="Interlingua" data-lang-code="ia">Interlingua</a></li><li><a class="language-link" data-tooltip="Ido" data-lang-code="io">Ido</a></li><li><a class="language-link" data-tooltip="Italian" data-lang-code="it">italiano</a></li><li><a class="language-link" data-tooltip="Japanese" data-lang-code="ja">日本語</a></li><li><a class="language-link" data-tooltip="Norwegian Bokmål" data-lang-code="nb">norsk bokmål</a></li><li><a class="language-link" data-tooltip="Dutch" data-lang-code="nl">Nederlands</a></li><li><a class="language-link" data-tooltip="Polish" data-lang-code="pl">polski</a></li><li><a class="language-link" data-tooltip="Portuguese (Brazil)" data-lang-code="pt_br">português (Brasil)</a></li><li><a class="language-link" data-tooltip="Portuguese" data-lang-code="pt">português</a></li><li><a class="language-link" data-tooltip="Russian" data-lang-code="ru">русский</a></li><li><a class="language-link" data-tooltip="Albanian" data-lang-code="sq">shqip</a></li><li><a class="language-link" data-tooltip="Swedish" data-lang-code="sv">svenska</a></li><li><a class="language-link" data-tooltip="Turkish" data-lang-code="tr">Türkçe</a></li><li><a class="language-link" data-tooltip="Ukrainian" data-lang-code="uk">українська</a></li><li><a class="language-link" data-tooltip="Chinese (China)" data-lang-code="zh_cn">中文(中国)</a></li><li><a class="language-link" data-tooltip="Chinese (Taiwan)" data-lang-code="zh_tw">中文(台灣)</a></li> </ul>
</li>
</ul>
</div>
</div>
</div>
</div>
<div id="no-js-error">Please enable javascript to use this site.<script>var element = document.getElementById('no-js-error'); element.parentNode.removeChild(element);</script></div>
<div id="page-container">
<div id="site-notice"><h1 style="color: white">WARNING: this site filled with KREMLIN TROLLS</h1><div id="site-notice-minimize">_</div></div> <div class="front-welcome-text "></div>
<div id="login-register-container">
<div id="login-content">
<form id="form_login" class="form_settings" action="https://shitposter.club/main/qlogin" method="post">
<div id="username-container">
<input id="nickname" name="nickname" type="text" value="" tabindex="1" />
</div>
<table class="password-signin"><tbody><tr>
<td class="flex-table-primary">
<div class="placeholding-input">
<input id="password" name="password" type="password" tabindex="2" value="" />
</div>
</td>
<td class="flex-table-secondary">
<button class="submit" type="submit" id="submit-login" tabindex="4"></button>
</td>
</tr></tbody></table>
<div id="remember-forgot">
<input type="checkbox" id="rememberme" name="rememberme" value="yes" tabindex="3" checked="checked"> <span id="rememberme_label"></span> · <a id="forgot-password" href="https://shitposter.club/main/recoverpassword" ></a>
<input type="hidden" id="token" name="token" value="99dbb9040190c2c0d1e0a991204b088116ba434cfcf532c2d423fdbd67647d1a2b737b446dfd81579980e6acd53ad37974801547b69f293e008f45bd5b89bc4a">
<a href="https://shitposter.club/main/openid" id="openid-login" title="OpenID" donthijack>OpenID</a> </div>
</form>
</div>
<div class="front-signup">
<h2></h2>
<div class="signup-input-container"><input placeholder="" type="text" name="user[name]" autocomplete="off" class="text-input" id="signup-user-name"></div>
<div class="signup-input-container"><input placeholder="" type="text" name="user[email]" autocomplete="off" id="signup-user-email"></div>
<div class="signup-input-container"><input placeholder="" type="password" name="user[user_password]" class="text-input" id="signup-user-password"></div>
<button id="signup-btn-step1" class="signup-btn" type="submit"></button>
</div>
<div id="other-servers-link"></div><div id="qvitter-notice-logged-out"></div></div>
<div id="feed">
<div id="feed-header">
<div id="feed-header-inner">
<h2>
<span id="stream-header"></span>
</h2>
<div class="reload-stream"></div>
</div>
<div id="feed-header-description"></div>
</div>
<div id="new-queets-bar-container" class="hidden"><div id="new-queets-bar"></div></div>
<div id="feed-body"></div>
</div>
<div id="hidden-html"><ol class="notices xoxo"><style type="text/css" media="">.greentext { color: green; }</style>
<style type="text/css" media="">
.sensitive-blocker {
display: none;
}
div.stream-item.notice.sensitive-notice .sensitive-blocker {
display: block;
width: 100%;
height: 100%;
position: absolute;
z-index: 100;
/*background-color: #d4baba;*/
background-color: black;
background-image: url(/custom/afterdark.jpg);
background-repeat: no-repeat;
background-position: center center;
background-size: contain;
transition: opacity 1s ease-in-out;
}
.sensitive-blocker:hover {
opacity: .5;
}
div.stream-item.notice.expanded.sensitive-notice .sensitive-blocker {
display: none;
background-color: transparent;
background-image: none;
}
</style>
<style type="text/css" media="">span.dicerolls { font-weight: bold; border: 1px solid black; }</style>
<li class="h-entry notice post notice-source-PleromaFE" id="notice-7369654">
<span class="tagcontainer">
<section class="notice-headers">
<a href="https://shitposter.club/notice/7369654" class="notice-title">shp (shpuld)'s status on Thursday, 22-Feb-2018 09:20:12 UTC</a>
<a href="https://shitposter.club/shpuld" class="h-card p-author" title="shpuld">
<img src="https://shitposter.club/avatar/5381-48-20171230093854.png" class="avatar u-photo" width="48" height="48" alt="shp"/>
shp</a>
<div class="parents">
<a href="https://testing.pleroma.lol/objects/b319022a-4946-44c5-9de9-34801f95507b" class="u-in-reply-to" rel="in-reply-to">in reply to</a>
<ul class="addressees">
<li class="h-card">
<a href="https://testing.pleroma.lol/users/lain" title="lain" class="addressee account">Rael Electric Razor</a>
</li>
</ul>
</div>
</section>
<article class="e-content">@<a href="https://testing.pleroma.lol/users/lain" class="h-card mention" title="Rael Electric Razor">lain</a> me far right</article>
<footer>
<a rel="bookmark" class="timestamp" href="https://shitposter.club/conversation/4378601#notice-7369654">
<time class="dt-published" datetime="2018-02-22T09:20:12+00:00" title="Thursday, 22-Feb-2018 09:20:12 UTC">about a day ago</time>
</a>
<span class="source">from <span class="device">Pleroma FE</span>
</span>
<a href="https://shitposter.club/notice/7369654" class="permalink u-url">permalink</a>
</footer>
</span>
</li>
</ol></div>
<div id="footer"><div id="footer-spinner-container"></div></div>
</div>
<script type="text/javascript" src="https://shitposter.club/plugins/Qvitter/js/lib/jquery-2.1.4.min.js?changed=20170610161937"></script>
<script type="text/javascript" src="https://shitposter.club/plugins/Qvitter/js/lib/jquery-ui.min.js?changed=20170610161937"></script>
<script type="text/javascript" src="https://shitposter.club/plugins/Qvitter/js/lib/jquery.minicolors.min.js?changed=20170610161937"></script>
<script type="text/javascript" src="https://shitposter.club/plugins/Qvitter/js/lib/jquery.jWindowCrop.js?changed=20170610161937"></script>
<script type="text/javascript" src="https://shitposter.club/plugins/Qvitter/js/lib/load-image.min.js?changed=20170610161937"></script>
<script type="text/javascript" src="https://shitposter.club/plugins/Qvitter/js/lib/xregexp-all-3.0.0-pre.js?changed=20170610161937"></script>
<script type="text/javascript" src="https://shitposter.club/plugins/Qvitter/js/lib/lz-string.js?changed=20170610161937"></script>
<script type="text/javascript" src="https://shitposter.club/plugins/Qvitter/js/lib/bowser.min.js?changed=20170610161937"></script>
<script charset="utf-8" type="text/javascript" src="https://shitposter.club/plugins/Qvitter/js/dom-functions.js?changed=20170830220115"></script>
<script charset="utf-8" type="text/javascript" src="https://shitposter.club/plugins/Qvitter/js/misc-functions.js?changed=20170610161937"></script>
<script charset="utf-8" type="text/javascript" src="https://shitposter.club/plugins/Qvitter/js/ajax-functions.js?changed=20170610161937"></script>
<script charset="utf-8" type="text/javascript" src="https://shitposter.club/plugins/Qvitter/js/stream-router.js?changed=20170610161937"></script>
<script charset="utf-8" type="text/javascript" src="https://shitposter.club/plugins/Qvitter/js/qvitter.js?changed=20170610161937"></script>
<link rel="stylesheet" type="text/css" href="/custom/spc.css">
<script src="/custom/spc.js"></script>
<meta property="og:image" content="http://shitposter.club/custom/ogimage.jpg" />
<meta property="og:title" content="Shitposter Club, a safe space on the Internet" />
<meta property="og:description" content="╔═════════════════
~ ~ ~ ~ ~ ~ ~ ~ ~ Reshare this if~ ~ ~ ~ ~ ~ ~ ~ ~
~ ~ ~ ~ We are a beautiful strong Social Media ~ ~ ~
~ ~ ~ ~ ~ ~ ~ who dont need no man ~ ~ ~ ~ ~ ~ ~
╚═════════════════ ೋღ☃ღೋ ════════════════╝" /><script src="https://shitposter.club/plugins/SPCEnhancements//js/audio-metadata.min.js"></script><script src='https://shitposter.club/plugins/QvitterSimpleSecurity/js/ss.js?changed=20160925025913'></script><style>
img.emoji {
width: auto;
height: 1.5em;
display: inline-block;
margin-bottom: -0.25em;
}
.queet-text {
padding-bottom: .25em;
}
</style>
<script src="https://shitposter.club/plugins/Emojify/js/emojify.js"></script>
<script>
emojify.setConfig({
img_dir: "https://shitposter.club/plugins/Emojify/images/emoji",
ignore_emoticons: true
});
var emojiReplacer = function(emoji, name, isEmoticon){
var classes = (isEmoticon ? "emoticon" : "emoji") + " emoji-" + name;
return '<span class="'+classes+'">'+emoji+'</span>';
}
</script>
<script src="https://shitposter.club/plugins/Emojify/js/qvitter_event.js"></script> <div id="dynamic-styles">
<style>
a, a:visited, a:active,
ul.stats li:hover a,
ul.stats li:hover a strong,
#user-body a:hover div strong,
#user-body a:hover div div,
.permalink-link:hover,
.stream-item.expanded > .queet .stream-item-expand,
.stream-item-footer .with-icn .requeet-text a b:hover,
.queet-text span.attachment.more,
.stream-item-header .created-at a:hover,
.stream-item-header a.account-group:hover .name,
.queet:hover .stream-item-expand,
.show-full-conversation:hover,
#new-queets-bar,
.menu-container div,
.cm-mention, .cm-tag, .cm-group, .cm-url, .cm-email,
div.syntax-middle span,
#user-body strong,
ul.stats,
.stream-item:not(.temp-post) ul.queet-actions li .icon:not(.is-mine):hover:before,
.show-full-conversation,
#user-body #user-queets:hover .label,
#user-body #user-groups:hover .label,
#user-body #user-following:hover .label,
ul.stats a strong,
.queet-box-extras button,
#openid-login:hover:after,
.post-to-group,
.stream-item-header .addressees .reply-to .h-card.not-mentioned-inline {
color:/*COLORSTART*/#0084B4/*COLOREND*/;
}
/*#unseen-notifications,*/
.stream-item.notification.not-seen > .queet::before,
#top-compose,
#logo,
.queet-toolbar button,
#user-header,
.profile-header-inner,
.topbar,
.menu-container,
.member-button.member,
.external-follow-button.following,
.qvitter-follow-button.following,
.save-profile-button,
.crop-and-save-button,
.topbar .global-nav.show-logo:before,
.topbar .global-nav.pulse-logo:before,
.dropdown-menu li:not(.dropdown-caret) a:hover {
background-color:/*BACKGROUNDCOLORSTART*/#0084B4/*BACKGROUNDCOLOREND*/;
}
.queet-box-syntax[contenteditable="true"]:focus,
.stream-item.selected-by-keyboard::before {
border-color:/*BORDERCOLORSTART*/#999999/*BORDERCOLOREND*/;
}
#user-footer-inner,
.inline-reply-queetbox,
#popup-faq #faq-container p.indent,
#find-someone {
background-color:/*LIGHTERBACKGROUNDCOLORSTART*/rgb(205,230,239)/*LIGHTERBACKGROUNDCOLOREND*/;
}
#user-footer-inner,
.queet-box,
.queet-box-syntax[contenteditable="true"],
.inline-reply-queetbox,
span.inline-reply-caret,
.stream-item.expanded .stream-item.first-visible-after-parent,
#popup-faq #faq-container p.indent,
.post-to-group,
.quoted-notice:hover,
.oembed-item:hover,
.stream-item:hover:not(.expanded) .quoted-notice:hover,
.stream-item:hover:not(.expanded) .oembed-item:hover,
#find-someone input:focus {
border-color:/*LIGHTERBORDERCOLORSTART*/rgb(155,206,224)/*LIGHTERBORDERCOLOREND*/;
}
span.inline-reply-caret .caret-inner {
border-bottom-color:/*LIGHTERBORDERBOTTOMCOLORSTART*/rgb(205,230,239)/*LIGHTERBORDERBOTTOMCOLOREND*/;
}
.modal-close .icon,
.chev-right,
.close-right,
button.icon.nav-search,
.member-button .join-text i,
.external-member-button .join-text i,
.external-follow-button .follow-text i,
.qvitter-follow-button .follow-text i,
#logo,
.upload-cover-photo,
.upload-avatar,
.upload-background-image,
button.shorten i,
.reload-stream,
.topbar .global-nav:before,
.stream-item.notification.repeat .dogear,
.stream-item.notification.like .dogear,
.ostatus-link,
.close-edit-profile-window {
background-image: url("../../custom/shitposter-sprite2.png?v=41");
background-size: 500px 1329px;
}
@media (max-width: 910px) {
#search-query,
.menu-container a,
.menu-container a.current,
.stream-selection.friends-timeline:after,
.stream-selection.notifications:after,
.stream-selection.my-timeline:after,
.stream-selection.public-and-external-timeline:after,
.stream-selection.public-timeline:after {
background-image: url("../../custom/shitposter-sprite2.png?v=41");
background-size: 500px 1329px;
}
}
</style>
</div>
</body>
</html>
<script type="text/javascript" src="https://shitposter.club/plugins/SensitiveContent/js/sensitivecontent.js"> </script>

View File

@ -0,0 +1 @@
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":"as:movedTo","Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji"}],"id":"http://mastodon.example.org/users/admin","type":"Person","following":"http://mastodon.example.org/users/admin/following","followers":"http://mastodon.example.org/users/admin/followers","inbox":"http://mastodon.example.org/users/admin/inbox","outbox":"http://mastodon.example.org/users/admin/outbox","preferredUsername":"admin","name":null,"summary":"\u003cp\u003e\u003c/p\u003e","url":"http://mastodon.example.org/@admin","manuallyApprovesFollowers":false,"publicKey":{"id":"http://mastodon.example.org/users/admin#main-key","owner":"http://mastodon.example.org/users/admin","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n"},"endpoints":{"sharedInbox":"http://mastodon.example.org/inbox"},"icon":{"type":"Image","mediaType":"image/jpeg","url":"https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"},"image":{"type":"Image","mediaType":"image/png","url":"https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"}}

View File

@ -0,0 +1 @@
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":"as:movedTo","Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji"}],"id":"https://masto.quad.moe/users/_HellPie","type":"Person","following":"https://masto.quad.moe/users/_HellPie/following","followers":"https://masto.quad.moe/users/_HellPie/followers","inbox":"https://masto.quad.moe/users/_HellPie/inbox","outbox":"https://masto.quad.moe/users/_HellPie/outbox","preferredUsername":"_HellPie","name":"_HellPie","summary":"\u003cp\u003eAndroid (Java) Developer, Linux addict. Often an asshole. Usually mentally ill, sometimes just retarded.\u003c/p\u003e\u003cp\u003eGitHub: \u003ca href=\"https://github.com/HellPie\" rel=\"nofollow noopener\" target=\"_blank\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003egithub.com/HellPie\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e\u003c/p\u003e","url":"https://masto.quad.moe/@_HellPie","manuallyApprovesFollowers":false,"publicKey":{"id":"https://masto.quad.moe/users/_HellPie#main-key","owner":"https://masto.quad.moe/users/_HellPie","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1fIReYnqpap6e3sIskIx\ni7q130EvfkSOTBTBe01w3Xb/7/JwzWgkmSp+sK5s/ImO2oZb3ljmKZ3iTg4ETtVa\nCrT98/5p4Hlw/Oozb0kTx+tUazrucr023u8lTmn5sVgksKue59gPzKEuJJT1Te7H\nPJg2frz4QZWEY9nuygJoDaWgLvq1aa4oRfctlpo2C4d4oKRZFx2wtgeGVpahsikX\nKFBWuvEMFL2LUWb44BkvN6bTmXL9ryQY2oRsWn0yZHnTvFItq4vkFSNNe6sK13pM\nOHu1rVJrKg2hNVpBowds9YqZM8zP9F0GS7SEARbwPRCaAGLJGNwLjfJolJ/231eU\nKQIDAQAB\n-----END PUBLIC KEY-----\n"},"endpoints":{"sharedInbox":"https://masto.quad.moe/inbox"},"icon":{"type":"Image","mediaType":"image/png","url":"https://masto.quad.moe/system/accounts/avatars/000/012/255/original/39b907e6b169191d.png"},"image":{"type":"Image","mediaType":"image/png","url":"https://masto.quad.moe/system/accounts/headers/000/012/255/original/8d3ace0025bdda431e07230668303945.png"}}

View File

@ -0,0 +1 @@
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":"as:movedTo","Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji"}],"id":"https://mstdn.io/users/mayuutann","type":"Person","following":"https://mstdn.io/users/mayuutann/following","followers":"https://mstdn.io/users/mayuutann/followers","inbox":"https://mstdn.io/users/mayuutann/inbox","outbox":"https://mstdn.io/users/mayuutann/outbox","preferredUsername":"mayuutann","name":"Mayutan☕","summary":"\u003cp\u003eI enjoy programming as a hobby.\u003cbr /\u003eJava.Ruby. Practicing English . I love karaoke.\u003cbr /\u003eAichi Japan.\u003cbr /\u003eI\u0026apos;d be glad if you pointed out it when my English is unnatural.\u003c/p\u003e","url":"https://mstdn.io/@mayuutann","manuallyApprovesFollowers":false,"publicKey":{"id":"https://mstdn.io/users/mayuutann#main-key","owner":"https://mstdn.io/users/mayuutann","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvz+MncrdPxQ5R99g9m8X\nY6QO1WNOsCj0wXuDmCHJxXfJx5NFYgsYSX3y2UTzoHNcxZIwbSy24HlYR44cEygy\nimiysTk3o0pVquXhFQNDBXJkAkPfY+9O/gz1FTbwEUzFS1m9zmoQUesDjHEBXvpW\nHkNRdVThsDHotiMYjd+WYS09XjCYxhUHcwsnEFZ+55y1Uz6OveY2OZH+jTEluF+s\nLLTDopY37Ogniah0zVm7Q+/WPdbjOullpWh8s/c5fYGl5xMaS950l5r4gkPU7MVE\n4dGSd/v4pUAxlZrhbRHrKMD4c9cmxn9gJuqmW49ZmPzIeG+SaLnad6zh0BN9nveR\njQIDAQAB\n-----END PUBLIC KEY-----\n"},"endpoints":{"sharedInbox":"https://mstdn.io/inbox"},"icon":{"type":"Image","mediaType":"image/jpeg","url":"https://mstdn.io/system/accounts/avatars/000/021/478/original/40fe303d51305ba4.jpg"},"image":{"type":"Image","mediaType":"image/jpeg","url":"https://mstdn.io/system/accounts/headers/000/021/478/original/4e1e9b5e1f350abb.jpg"}}

View File

@ -0,0 +1 @@
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":"as:movedTo","Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji"}],"id":"https://mstdn.io/users/mayuutann/statuses/99568293732299394","type":"Note","summary":null,"content":"\u003cp\u003e\u003cspan class=\"h-card\"\u003e\u003ca href=\"https://shitposter.club/shpuld\" class=\"u-url mention\"\u003e@\u003cspan\u003eshpuld\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e \u003cspan class=\"h-card\"\u003e\u003ca href=\"https://testing.pleroma.lol/users/lain\" class=\"u-url mention\"\u003e@\u003cspan\u003elain\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e ポポポォォォ\u003c/p\u003e","inReplyTo":"https://shitposter.club/notice/7369654","published":"2018-02-22T09:26:31Z","url":"https://mstdn.io/@mayuutann/99568293732299394","attributedTo":"https://mstdn.io/users/mayuutann","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://mstdn.io/users/mayuutann/followers","https://testing.pleroma.lol/users/lain","https://shitposter.club/user/5381"],"sensitive":false,"atomUri":"https://mstdn.io/users/mayuutann/statuses/99568293732299394","inReplyToAtomUri":"tag:shitposter.club,2018-02-22:noticeId=7369654:objectType=comment","conversation":"tag:shitposter.club,2018-02-22:objectType=thread:nonce=e5a7c72d60a9c0e4","attachment":[],"tag":[{"type":"Mention","href":"https://testing.pleroma.lol/users/lain","name":"@lain@testing.pleroma.lol"},{"type":"Mention","href":"https://shitposter.club/user/5381","name":"@shpuld@shitposter.club"}]}

1
test/fixtures/httpoison_mock/rye.json vendored Normal file
View File

@ -0,0 +1 @@
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":"as:movedTo","Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji"}],"id":"https://niu.moe/users/rye","type":"Person","following":"https://niu.moe/users/rye/following","followers":"https://niu.moe/users/rye/followers","inbox":"https://niu.moe/users/rye/inbox","outbox":"https://niu.moe/users/rye/outbox","preferredUsername":"rye","name":"♡ rye ♡","summary":"\u003cp\u003elettuce club champion\u003c/p\u003e\u003cp\u003eicon by gomigomipomi\u003c/p\u003e","url":"https://niu.moe/@rye","manuallyApprovesFollowers":false,"publicKey":{"id":"https://niu.moe/users/rye#main-key","owner":"https://niu.moe/users/rye","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA83uRWjCFO35FwfA38mzv\nEL0TUaXB7+2hYvPwNrn1WY6me5DRbqB5zzMrzWMGr0HSooqNqEYBafGsmVTWUqIk\nKM9ehtIBraJI+mT5X7DPR3LrXOJF4a9EEslg8XvAk8MN9IrAhm6UljnvB67RtDcA\nTNB01VWy9yWnxFRtz9o/EMoBPyw5giOaXE2ibVNP8lQIqGKuuBKPzPjSJygdvQ5q\nxfow2z1TpKRqdsNDqn4n6U6zCXYTzkr0J71/tGw7fsgfv78l0Wjrc7EcuBk74OaG\nC65UDiu3X4Q6kxCfCEhPSfuwLN+UZkzxcn6goWR0iYpWs57+4tFKu9nJYP4QJ0K9\nTwIDAQAB\n-----END PUBLIC KEY-----\n"},"endpoints":{"sharedInbox":"https://niu.moe/inbox"},"icon":{"type":"Image","mediaType":"image/jpeg","url":"https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"},"image":{"type":"Image","mediaType":"image/png","url":"https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"}}

View File

@ -0,0 +1,438 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/">
<generator uri="https://gnu.io/social" version="1.2.0-beta4">GNU social</generator>
<id>https://shitposter.club/api/statuses/user_timeline/5381.atom</id>
<title>shpuld timeline</title>
<subtitle>Updates from shpuld on Shitposter Club!</subtitle>
<logo>https://shitposter.club/avatar/5381-96-20171230093854.png</logo>
<updated>2018-02-23T13:42:22+00:00</updated>
<author>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<uri>https://shitposter.club/user/5381</uri>
<name>shpuld</name>
<link rel="alternate" type="text/html" href="https://shitposter.club/shpuld"/>
<link rel="avatar" type="image/png" media:width="864" media:height="864" href="https://shitposter.club/avatar/5381-original-20171230093854.png"/>
<link rel="avatar" type="image/png" media:width="96" media:height="96" href="https://shitposter.club/avatar/5381-96-20171230093854.png"/>
<link rel="avatar" type="image/png" media:width="48" media:height="48" href="https://shitposter.club/avatar/5381-48-20171230093854.png"/>
<link rel="avatar" type="image/png" media:width="24" media:height="24" href="https://shitposter.club/avatar/5381-24-20171230093900.png"/>
<poco:preferredUsername>shpuld</poco:preferredUsername>
<poco:displayName>shp</poco:displayName>
<followers url="https://shitposter.club/shpuld/subscribers"></followers>
<statusnet:profile_info local_id="5381"></statusnet:profile_info>
</author>
<link href="https://shitposter.club/shpuld" rel="alternate" type="text/html"/>
<link href="https://shitposter.club/main/sup" rel="http://api.friendfeed.com/2008/03#sup" type="application/json"/>
<link href="https://shitposter.club/api/statuses/user_timeline/5381.atom?max_id=7387342" rel="next" type="application/atom+xml"/>
<link href="https://shitposter.club/main/push/hub" rel="hub"/>
<link href="https://shitposter.club/main/salmon/user/5381" rel="salmon"/>
<link href="https://shitposter.club/main/salmon/user/5381" rel="http://salmon-protocol.org/ns/salmon-replies"/>
<link href="https://shitposter.club/main/salmon/user/5381" rel="http://salmon-protocol.org/ns/salmon-mention"/>
<link href="https://shitposter.club/api/statuses/user_timeline/5381.atom" rel="self" type="application/atom+xml"/>
<entry>
<id>tag:shitposter.club,2018-02-23:fave:5381:comment:7387801:2018-02-23T13:39:40+00:00</id>
<title>Favorite</title>
<content type="html">shpuld favorited something by mayuutann: &lt;p&gt;&lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://freezepeach.xyz/hakui&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;hakui&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; &lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://gs.smuglo.li/histoire&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;histoire&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; &lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://shitposter.club/shpuld&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;shpuld&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; &lt;a href=&quot;https://mstdn.io/media/_Ee-x91XN0udpfZVO_U&quot; rel=&quot;nofollow&quot;&gt;&lt;span class=&quot;invisible&quot;&gt;https://&lt;/span&gt;&lt;span class=&quot;ellipsis&quot;&gt;mstdn.io/media/_Ee-x91XN0udpfZ&lt;/span&gt;&lt;span class=&quot;invisible&quot;&gt;VO_U&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387804"/>
<activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
<published>2018-02-23T13:39:40+00:00</published>
<updated>2018-02-23T13:39:40+00:00</updated>
<activity:object>
<activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
<id>https://mstdn.io/users/mayuutann/statuses/99574950785668071</id>
<title>New comment by mayuutann</title>
<content type="html">&lt;p&gt;&lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://freezepeach.xyz/hakui&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;hakui&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; &lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://gs.smuglo.li/histoire&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;histoire&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; &lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://shitposter.club/shpuld&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;shpuld&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; &lt;a href=&quot;https://mstdn.io/media/_Ee-x91XN0udpfZVO_U&quot; rel=&quot;nofollow&quot;&gt;&lt;span class=&quot;invisible&quot;&gt;https://&lt;/span&gt;&lt;span class=&quot;ellipsis&quot;&gt;mstdn.io/media/_Ee-x91XN0udpfZ&lt;/span&gt;&lt;span class=&quot;invisible&quot;&gt;VO_U&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</content>
<link rel="alternate" type="text/html" href="https://mstdn.io/@mayuutann/99574950785668071"/>
<status_net notice_id="7387801"></status_net>
</activity:object>
<thr:in-reply-to ref="https://mstdn.io/users/mayuutann/statuses/99574950785668071" href="https://mstdn.io/@mayuutann/99574950785668071"></thr:in-reply-to>
<link rel="related" href="https://mstdn.io/@mayuutann/99574950785668071"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4389848"/>
<ostatus:conversation href="https://shitposter.club/conversation/4389848" local_id="4389848" ref="https://freezepeach.xyz/conversation/4182511">https://freezepeach.xyz/conversation/4182511</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387804.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387804.atom"/>
<statusnet:notice_info local_id="7387804" source="unknown"></statusnet:notice_info>
</entry>
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
<id>tag:shitposter.club,2018-02-23:noticeId=7387723:objectType=comment</id>
<title>New comment by shpuld</title>
<content type="html">@&lt;a href=&quot;https://freezepeach.xyz/user/3458&quot; class=&quot;h-card mention&quot; title=&quot;&amp;#x5FA1;&amp;#x5712;&amp;#x306F;&amp;#x304F;&amp;#x3044;&quot;&gt;hakui&lt;/a&gt; @&lt;a href=&quot;https://pleroma.soykaf.com/users/lain&quot; class=&quot;h-card mention&quot; title=&quot;&amp;#x2468; lain &amp;#x2468;&quot;&gt;lain&lt;/a&gt; how naive~</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387723"/>
<status_net notice_id="7387723"></status_net>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<published>2018-02-23T13:30:15+00:00</published>
<updated>2018-02-23T13:30:15+00:00</updated>
<thr:in-reply-to ref="tag:freezepeach.xyz,2018-02-23:noticeId=6451587:objectType=comment" href="https://freezepeach.xyz/notice/6451587"></thr:in-reply-to>
<link rel="related" href="https://freezepeach.xyz/notice/6451587"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4389967"/>
<ostatus:conversation href="https://shitposter.club/conversation/4389967" local_id="4389967" ref="tag:shitposter.club,2018-02-23:objectType=thread:nonce=2f09acf104aebfe3">tag:shitposter.club,2018-02-23:objectType=thread:nonce=2f09acf104aebfe3</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://freezepeach.xyz/user/3458"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://pleroma.soykaf.com/users/lain"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387723.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387723.atom"/>
<statusnet:notice_info local_id="7387723" source="Pleroma FE"></statusnet:notice_info>
</entry>
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
<id>tag:shitposter.club,2018-02-23:noticeId=7387703:objectType=comment</id>
<title>New comment by shpuld</title>
<content type="html">@&lt;a href=&quot;https://freezepeach.xyz/user/3458&quot; class=&quot;h-card mention&quot; title=&quot;&amp;#x5FA1;&amp;#x5712;&amp;#x306F;&amp;#x304F;&amp;#x3044;&quot;&gt;hakui&lt;/a&gt; @&lt;a href=&quot;https://pleroma.soykaf.com/users/lain&quot; class=&quot;h-card mention&quot; title=&quot;&amp;#x2468; lain &amp;#x2468;&quot;&gt;lain&lt;/a&gt; you expect anyone to believe that??</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387703"/>
<status_net notice_id="7387703"></status_net>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<published>2018-02-23T13:28:08+00:00</published>
<updated>2018-02-23T13:28:08+00:00</updated>
<thr:in-reply-to ref="tag:freezepeach.xyz,2018-02-23:noticeId=6451569:objectType=comment" href="https://freezepeach.xyz/notice/6451569"></thr:in-reply-to>
<link rel="related" href="https://freezepeach.xyz/notice/6451569"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4389967"/>
<ostatus:conversation href="https://shitposter.club/conversation/4389967" local_id="4389967" ref="tag:shitposter.club,2018-02-23:objectType=thread:nonce=2f09acf104aebfe3">tag:shitposter.club,2018-02-23:objectType=thread:nonce=2f09acf104aebfe3</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://freezepeach.xyz/user/3458"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://pleroma.soykaf.com/users/lain"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387703.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387703.atom"/>
<statusnet:notice_info local_id="7387703" source="Pleroma FE"></statusnet:notice_info>
</entry>
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
<id>tag:shitposter.club,2018-02-23:noticeId=7387639:objectType=comment</id>
<title>New comment by shpuld</title>
<content type="html">@&lt;a href=&quot;https://mstdn.io/users/mayuutann&quot; class=&quot;h-card mention&quot; title=&quot;Mayutan&amp;#x2615;&quot;&gt;mayuutann&lt;/a&gt; @&lt;a href=&quot;https://freezepeach.xyz/user/3458&quot; class=&quot;h-card mention&quot; title=&quot;&amp;#x5FA1;&amp;#x5712;&amp;#x306F;&amp;#x304F;&amp;#x3044;&quot;&gt;hakui&lt;/a&gt; pacyuri!! &lt;a href=&quot;https://shitposter.club/file/eea140be45df3f993c4533026bf9a78fe8facd296d2fa0c6d02b2e347c5dc30e.jpg&quot; title=&quot;https://shitposter.club/file/eea140be45df3f993c4533026bf9a78fe8facd296d2fa0c6d02b2e347c5dc30e.jpg&quot; class=&quot;attachment&quot; id=&quot;attachment-1589462&quot; rel=&quot;nofollow external&quot;&gt;https://shitposter.club/attachment/1589462&lt;/a&gt;</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387639"/>
<status_net notice_id="7387639"></status_net>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<published>2018-02-23T13:20:38+00:00</published>
<updated>2018-02-23T13:20:38+00:00</updated>
<thr:in-reply-to ref="https://mstdn.io/users/mayuutann/statuses/99574870416888767" href="https://mstdn.io/@mayuutann/99574870416888767"></thr:in-reply-to>
<link rel="related" href="https://mstdn.io/@mayuutann/99574870416888767"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4390261"/>
<ostatus:conversation href="https://shitposter.club/conversation/4390261" local_id="4390261" ref="https://freezepeach.xyz/conversation/4183220">https://freezepeach.xyz/conversation/4183220</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://freezepeach.xyz/user/3458"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mstdn.io/users/mayuutann"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="enclosure" href="https://shitposter.club/file/eea140be45df3f993c4533026bf9a78fe8facd296d2fa0c6d02b2e347c5dc30e.jpg" type="image/jpeg" length="42186"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387639.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387639.atom"/>
<statusnet:notice_info local_id="7387639" source="Pleroma FE"></statusnet:notice_info>
</entry>
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
<id>tag:shitposter.club,2018-02-23:noticeId=7387611:objectType=comment</id>
<title>New comment by shpuld</title>
<content type="html">@&lt;a href=&quot;https://freezepeach.xyz/user/3458&quot; class=&quot;h-card mention&quot; title=&quot;&amp;#x5FA1;&amp;#x5712;&amp;#x306F;&amp;#x304F;&amp;#x3044;&quot;&gt;hakui&lt;/a&gt; why is pacyu eating a pizza so cute</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387611"/>
<status_net notice_id="7387611"></status_net>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<published>2018-02-23T13:18:07+00:00</published>
<updated>2018-02-23T13:18:07+00:00</updated>
<thr:in-reply-to ref="tag:freezepeach.xyz,2018-02-23:noticeId=6451402:objectType=comment" href="https://freezepeach.xyz/notice/6451402"></thr:in-reply-to>
<link rel="related" href="https://freezepeach.xyz/notice/6451402"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4390261"/>
<ostatus:conversation href="https://shitposter.club/conversation/4390261" local_id="4390261" ref="https://freezepeach.xyz/conversation/4183220">https://freezepeach.xyz/conversation/4183220</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://freezepeach.xyz/user/3458"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387611.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387611.atom"/>
<statusnet:notice_info local_id="7387611" source="Pleroma FE"></statusnet:notice_info>
</entry>
<entry>
<id>tag:shitposter.club,2018-02-23:fave:5381:comment:7387600:2018-02-23T13:17:52+00:00</id>
<title>Favorite</title>
<content type="html">shpuld favorited something by mayuutann: &lt;p&gt;&lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://shitposter.club/shpuld&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;shpuld&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; &lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://gs.smuglo.li/histoire&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;histoire&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; &lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://freezepeach.xyz/hakui&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;hakui&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; pichu! &lt;a href=&quot;https://mstdn.io/media/Crv5eubz1KO0dgBEulI&quot; rel=&quot;nofollow&quot;&gt;&lt;span class=&quot;invisible&quot;&gt;https://&lt;/span&gt;&lt;span class=&quot;ellipsis&quot;&gt;mstdn.io/media/Crv5eubz1KO0dgB&lt;/span&gt;&lt;span class=&quot;invisible&quot;&gt;EulI&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387606"/>
<activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
<published>2018-02-23T13:17:52+00:00</published>
<updated>2018-02-23T13:17:52+00:00</updated>
<activity:object>
<activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
<id>https://mstdn.io/users/mayuutann/statuses/99574863865459283</id>
<title>New comment by mayuutann</title>
<content type="html">&lt;p&gt;&lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://shitposter.club/shpuld&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;shpuld&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; &lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://gs.smuglo.li/histoire&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;histoire&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; &lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://freezepeach.xyz/hakui&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;hakui&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; pichu! &lt;a href=&quot;https://mstdn.io/media/Crv5eubz1KO0dgBEulI&quot; rel=&quot;nofollow&quot;&gt;&lt;span class=&quot;invisible&quot;&gt;https://&lt;/span&gt;&lt;span class=&quot;ellipsis&quot;&gt;mstdn.io/media/Crv5eubz1KO0dgB&lt;/span&gt;&lt;span class=&quot;invisible&quot;&gt;EulI&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</content>
<link rel="alternate" type="text/html" href="https://mstdn.io/@mayuutann/99574863865459283"/>
<status_net notice_id="7387600"></status_net>
</activity:object>
<thr:in-reply-to ref="https://mstdn.io/users/mayuutann/statuses/99574863865459283" href="https://mstdn.io/@mayuutann/99574863865459283"></thr:in-reply-to>
<link rel="related" href="https://mstdn.io/@mayuutann/99574863865459283"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4389848"/>
<ostatus:conversation href="https://shitposter.club/conversation/4389848" local_id="4389848" ref="https://freezepeach.xyz/conversation/4182511">https://freezepeach.xyz/conversation/4182511</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387606.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387606.atom"/>
<statusnet:notice_info local_id="7387606" source="unknown"></statusnet:notice_info>
</entry>
<entry>
<id>tag:shitposter.club,2018-02-23:fave:5381:comment:7387544:2018-02-23T13:12:43+00:00</id>
<title>Favorite</title>
<content type="html">shpuld favorited something by mayuutann: &lt;p&gt;&lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://shitposter.club/shpuld&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;shpuld&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; wa~~i!! :blobcheer:&lt;/p&gt;</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387557"/>
<activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
<published>2018-02-23T13:12:43+00:00</published>
<updated>2018-02-23T13:12:43+00:00</updated>
<activity:object>
<activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
<id>https://mstdn.io/users/mayuutann/statuses/99574840290947233</id>
<title>New comment by mayuutann</title>
<content type="html">&lt;p&gt;&lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://shitposter.club/shpuld&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;shpuld&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; wa~~i!! :blobcheer:&lt;/p&gt;</content>
<link rel="alternate" type="text/html" href="https://mstdn.io/@mayuutann/99574840290947233"/>
<status_net notice_id="7387544"></status_net>
</activity:object>
<thr:in-reply-to ref="https://mstdn.io/users/mayuutann/statuses/99574840290947233" href="https://mstdn.io/@mayuutann/99574840290947233"></thr:in-reply-to>
<link rel="related" href="https://mstdn.io/@mayuutann/99574840290947233"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4390030"/>
<ostatus:conversation href="https://shitposter.club/conversation/4390030" local_id="4390030" ref="tag:shitposter.club,2018-02-23:objectType=thread:nonce=d05e2b056274c5ab">tag:shitposter.club,2018-02-23:objectType=thread:nonce=d05e2b056274c5ab</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387557.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387557.atom"/>
<statusnet:notice_info local_id="7387557" source="unknown"></statusnet:notice_info>
</entry>
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
<id>tag:shitposter.club,2018-02-23:noticeId=7387555:objectType=comment</id>
<title>New comment by shpuld</title>
<content type="html">@&lt;a href=&quot;https://freezepeach.xyz/user/3458&quot; class=&quot;h-card mention&quot; title=&quot;&amp;#x5FA1;&amp;#x5712;&amp;#x306F;&amp;#x304F;&amp;#x3044;&quot;&gt;hakui&lt;/a&gt; more!!</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387555"/>
<status_net notice_id="7387555"></status_net>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<published>2018-02-23T13:12:23+00:00</published>
<updated>2018-02-23T13:12:23+00:00</updated>
<thr:in-reply-to ref="tag:freezepeach.xyz,2018-02-23:noticeId=6451332:objectType=note" href="https://freezepeach.xyz/notice/6451332"></thr:in-reply-to>
<link rel="related" href="https://freezepeach.xyz/notice/6451332"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4390261"/>
<ostatus:conversation href="https://shitposter.club/conversation/4390261" local_id="4390261" ref="https://freezepeach.xyz/conversation/4183220">https://freezepeach.xyz/conversation/4183220</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://freezepeach.xyz/user/3458"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387555.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387555.atom"/>
<statusnet:notice_info local_id="7387555" source="Pleroma FE"></statusnet:notice_info>
</entry>
<entry>
<id>tag:shitposter.club,2018-02-23:fave:5381:note:7387537:2018-02-23T13:12:19+00:00</id>
<title>Favorite</title>
<content type="html">shpuld favorited something by hakui: you have pacyupacyu'd for: 45 minutes 03 seconds</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387553"/>
<activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
<published>2018-02-23T13:12:19+00:00</published>
<updated>2018-02-23T13:12:19+00:00</updated>
<activity:object>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<id>tag:freezepeach.xyz,2018-02-23:noticeId=6451332:objectType=note</id>
<title>New note by hakui</title>
<content type="html">you have pacyupacyu'd for: 45 minutes 03 seconds</content>
<link rel="alternate" type="text/html" href="https://freezepeach.xyz/notice/6451332"/>
<status_net notice_id="7387537"></status_net>
</activity:object>
<thr:in-reply-to ref="tag:freezepeach.xyz,2018-02-23:noticeId=6451332:objectType=note" href="https://freezepeach.xyz/notice/6451332"></thr:in-reply-to>
<link rel="related" href="https://freezepeach.xyz/notice/6451332"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4390261"/>
<ostatus:conversation href="https://shitposter.club/conversation/4390261" local_id="4390261" ref="https://freezepeach.xyz/conversation/4183220">https://freezepeach.xyz/conversation/4183220</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387553.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387553.atom"/>
<statusnet:notice_info local_id="7387553" source="unknown"></statusnet:notice_info>
</entry>
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
<id>tag:shitposter.club,2018-02-23:noticeId=7387539:objectType=comment</id>
<title>New comment by shpuld</title>
<content type="html">@&lt;a href=&quot;https://mstdn.io/users/mayuutann&quot; class=&quot;h-card mention&quot; title=&quot;Mayutan&amp;#x2615;&quot;&gt;mayuutann&lt;/a&gt; ndndnd~</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387539"/>
<status_net notice_id="7387539"></status_net>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<published>2018-02-23T13:11:04+00:00</published>
<updated>2018-02-23T13:11:04+00:00</updated>
<thr:in-reply-to ref="https://mstdn.io/users/mayuutann/statuses/99574837619821505" href="https://mstdn.io/@mayuutann/99574837619821505"></thr:in-reply-to>
<link rel="related" href="https://mstdn.io/@mayuutann/99574837619821505"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4390030"/>
<ostatus:conversation href="https://shitposter.club/conversation/4390030" local_id="4390030" ref="tag:shitposter.club,2018-02-23:objectType=thread:nonce=d05e2b056274c5ab">tag:shitposter.club,2018-02-23:objectType=thread:nonce=d05e2b056274c5ab</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mstdn.io/users/mayuutann"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387539.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387539.atom"/>
<statusnet:notice_info local_id="7387539" source="Pleroma FE"></statusnet:notice_info>
</entry>
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
<id>tag:shitposter.club,2018-02-23:noticeId=7387518:objectType=comment</id>
<title>New comment by shpuld</title>
<content type="html">@&lt;a href=&quot;https://mstdn.io/users/mayuutann&quot; class=&quot;h-card mention&quot; title=&quot;Mayutan&amp;#x2615;&quot;&gt;mayuutann&lt;/a&gt; well done! mayumayu is so energetic</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387518"/>
<status_net notice_id="7387518"></status_net>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<published>2018-02-23T13:08:50+00:00</published>
<updated>2018-02-23T13:08:50+00:00</updated>
<thr:in-reply-to ref="https://mstdn.io/users/mayuutann/statuses/99574826506801503" href="https://mstdn.io/@mayuutann/99574826506801503"></thr:in-reply-to>
<link rel="related" href="https://mstdn.io/@mayuutann/99574826506801503"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4390030"/>
<ostatus:conversation href="https://shitposter.club/conversation/4390030" local_id="4390030" ref="tag:shitposter.club,2018-02-23:objectType=thread:nonce=d05e2b056274c5ab">tag:shitposter.club,2018-02-23:objectType=thread:nonce=d05e2b056274c5ab</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mstdn.io/users/mayuutann"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387518.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387518.atom"/>
<statusnet:notice_info local_id="7387518" source="Pleroma FE"></statusnet:notice_info>
</entry>
<entry>
<id>tag:shitposter.club,2018-02-23:fave:5381:note:7387503:2018-02-23T13:08:00+00:00</id>
<title>Favorite</title>
<content type="html">shpuld favorited something by mayuutann: &lt;p&gt;done with FIGURE MAT!!&lt;br /&gt; (Posted with IFTTT)&lt;/p&gt;</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387511"/>
<activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
<published>2018-02-23T13:08:00+00:00</published>
<updated>2018-02-23T13:08:00+00:00</updated>
<activity:object>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<id>https://mstdn.io/users/mayuutann/statuses/99574825526201897</id>
<title>New note by mayuutann</title>
<content type="html">&lt;p&gt;done with FIGURE MAT!!&lt;br /&gt; (Posted with IFTTT)&lt;/p&gt;</content>
<link rel="alternate" type="text/html" href="https://mstdn.io/@mayuutann/99574825526201897"/>
<status_net notice_id="7387503"></status_net>
</activity:object>
<thr:in-reply-to ref="https://mstdn.io/users/mayuutann/statuses/99574825526201897" href="https://mstdn.io/@mayuutann/99574825526201897"></thr:in-reply-to>
<link rel="related" href="https://mstdn.io/@mayuutann/99574825526201897"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4390240"/>
<ostatus:conversation href="https://shitposter.club/conversation/4390240" local_id="4390240" ref="tag:shitposter.club,2018-02-23:objectType=thread:nonce=c6aaa9b91e8d242f">tag:shitposter.club,2018-02-23:objectType=thread:nonce=c6aaa9b91e8d242f</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387511.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387511.atom"/>
<statusnet:notice_info local_id="7387511" source="unknown"></statusnet:notice_info>
</entry>
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
<id>tag:shitposter.club,2018-02-23:noticeId=7387486:objectType=comment</id>
<title>New comment by shpuld</title>
<content type="html">@&lt;a href=&quot;https://freezepeach.xyz/user/3458&quot; class=&quot;h-card mention&quot; title=&quot;&amp;#x5FA1;&amp;#x5712;&amp;#x306F;&amp;#x304F;&amp;#x3044;&quot;&gt;hakui&lt;/a&gt; @&lt;a href=&quot;https://a.weirder.earth/users/mutstd&quot; class=&quot;h-card mention&quot; title=&quot;Mutant Standard&quot;&gt;mutstd&lt;/a&gt; @&lt;a href=&quot;https://donphan.social/users/Siphonay&quot; class=&quot;h-card mention&quot; title=&quot;Siphonay&quot;&gt;siphonay&lt;/a&gt; jokes on you I'm oppressively shitposting myself</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387486"/>
<status_net notice_id="7387486"></status_net>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<published>2018-02-23T13:05:44+00:00</published>
<updated>2018-02-23T13:05:44+00:00</updated>
<thr:in-reply-to ref="tag:freezepeach.xyz,2018-02-23:noticeId=6451272:objectType=comment" href="https://freezepeach.xyz/notice/6451272"></thr:in-reply-to>
<link rel="related" href="https://freezepeach.xyz/notice/6451272"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4389665"/>
<ostatus:conversation href="https://shitposter.club/conversation/4389665" local_id="4389665" ref="tag:shitposter.club,2018-02-23:objectType=thread:nonce=5d306467336c9661">tag:shitposter.club,2018-02-23:objectType=thread:nonce=5d306467336c9661</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://freezepeach.xyz/user/3458"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://a.weirder.earth/users/mutstd"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://donphan.social/users/Siphonay"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387486.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387486.atom"/>
<statusnet:notice_info local_id="7387486" source="Pleroma FE"></statusnet:notice_info>
</entry>
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
<id>tag:shitposter.club,2018-02-23:noticeId=7387466:objectType=comment</id>
<title>New comment by shpuld</title>
<content type="html">@&lt;a href=&quot;https://freezepeach.xyz/user/3458&quot; class=&quot;h-card mention&quot; title=&quot;&amp;#x5FA1;&amp;#x5712;&amp;#x306F;&amp;#x304F;&amp;#x3044;&quot;&gt;hakui&lt;/a&gt; @&lt;a href=&quot;https://a.weirder.earth/users/mutstd&quot; class=&quot;h-card mention&quot; title=&quot;Mutant Standard&quot;&gt;mutstd&lt;/a&gt; @&lt;a href=&quot;https://donphan.social/users/Siphonay&quot; class=&quot;h-card mention&quot; title=&quot;Siphonay&quot;&gt;siphonay&lt;/a&gt; how does it feel being hostile</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387466"/>
<status_net notice_id="7387466"></status_net>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<published>2018-02-23T13:04:10+00:00</published>
<updated>2018-02-23T13:04:10+00:00</updated>
<thr:in-reply-to ref="tag:freezepeach.xyz,2018-02-23:noticeId=6451260:objectType=comment" href="https://freezepeach.xyz/notice/6451260"></thr:in-reply-to>
<link rel="related" href="https://freezepeach.xyz/notice/6451260"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4389665"/>
<ostatus:conversation href="https://shitposter.club/conversation/4389665" local_id="4389665" ref="tag:shitposter.club,2018-02-23:objectType=thread:nonce=5d306467336c9661">tag:shitposter.club,2018-02-23:objectType=thread:nonce=5d306467336c9661</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://freezepeach.xyz/user/3458"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://a.weirder.earth/users/mutstd"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://donphan.social/users/Siphonay"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387466.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387466.atom"/>
<statusnet:notice_info local_id="7387466" source="Pleroma FE"></statusnet:notice_info>
</entry>
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
<id>tag:shitposter.club,2018-02-23:noticeId=7387459:objectType=comment</id>
<title>New comment by shpuld</title>
<content type="html">@&lt;a href=&quot;https://freezepeach.xyz/user/3458&quot; class=&quot;h-card mention&quot; title=&quot;&amp;#x5FA1;&amp;#x5712;&amp;#x306F;&amp;#x304F;&amp;#x3044;&quot;&gt;hakui&lt;/a&gt; gorogoro</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387459"/>
<status_net notice_id="7387459"></status_net>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<published>2018-02-23T13:03:32+00:00</published>
<updated>2018-02-23T13:03:32+00:00</updated>
<thr:in-reply-to ref="tag:freezepeach.xyz,2018-02-23:noticeId=6451248:objectType=comment" href="https://freezepeach.xyz/notice/6451248"></thr:in-reply-to>
<link rel="related" href="https://freezepeach.xyz/notice/6451248"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4389271"/>
<ostatus:conversation href="https://shitposter.club/conversation/4389271" local_id="4389271" ref="https://freezepeach.xyz/conversation/4181784">https://freezepeach.xyz/conversation/4181784</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://freezepeach.xyz/user/3458"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387459.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387459.atom"/>
<statusnet:notice_info local_id="7387459" source="Pleroma FE"></statusnet:notice_info>
</entry>
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
<id>tag:shitposter.club,2018-02-23:noticeId=7387432:objectType=comment</id>
<title>New comment by shpuld</title>
<content type="html">@&lt;a href=&quot;https://freezepeach.xyz/user/3458&quot; class=&quot;h-card mention&quot; title=&quot;&amp;#x5FA1;&amp;#x5712;&amp;#x306F;&amp;#x304F;&amp;#x3044;&quot;&gt;hakui&lt;/a&gt; ndnd</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387432"/>
<status_net notice_id="7387432"></status_net>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<published>2018-02-23T13:02:05+00:00</published>
<updated>2018-02-23T13:02:05+00:00</updated>
<thr:in-reply-to ref="tag:freezepeach.xyz,2018-02-23:noticeId=6451204:objectType=comment" href="https://freezepeach.xyz/notice/6451204"></thr:in-reply-to>
<link rel="related" href="https://freezepeach.xyz/notice/6451204"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4389271"/>
<ostatus:conversation href="https://shitposter.club/conversation/4389271" local_id="4389271" ref="https://freezepeach.xyz/conversation/4181784">https://freezepeach.xyz/conversation/4181784</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://freezepeach.xyz/user/3458"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387432.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387432.atom"/>
<statusnet:notice_info local_id="7387432" source="Pleroma FE"></statusnet:notice_info>
</entry>
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<id>tag:shitposter.club,2018-02-23:noticeId=7387367:objectType=note</id>
<title>New note by shpuld</title>
<content type="html">dear diary: I'm trying to do work but I can only think of tenshi eating a corndog</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387367"/>
<status_net notice_id="7387367"></status_net>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<published>2018-02-23T12:56:03+00:00</published>
<updated>2018-02-23T12:56:03+00:00</updated>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4390142"/>
<ostatus:conversation href="https://shitposter.club/conversation/4390142" local_id="4390142" ref="tag:shitposter.club,2018-02-23:objectType=thread:nonce=57f316da416743fc">tag:shitposter.club,2018-02-23:objectType=thread:nonce=57f316da416743fc</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387367.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387367.atom"/>
<statusnet:notice_info local_id="7387367" source="Pleroma FE"></statusnet:notice_info>
</entry>
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<id>tag:shitposter.club,2018-02-23:noticeId=7387354:objectType=note</id>
<title>New note by shpuld</title>
<content type="html">jesus christ it's such a fridey at work</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387354"/>
<status_net notice_id="7387354"></status_net>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<published>2018-02-23T12:53:50+00:00</published>
<updated>2018-02-23T12:53:50+00:00</updated>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4390131"/>
<ostatus:conversation href="https://shitposter.club/conversation/4390131" local_id="4390131" ref="tag:shitposter.club,2018-02-23:objectType=thread:nonce=c05eb5e91bdcbdb7">tag:shitposter.club,2018-02-23:objectType=thread:nonce=c05eb5e91bdcbdb7</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387354.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387354.atom"/>
<statusnet:notice_info local_id="7387354" source="Pleroma FE"></statusnet:notice_info>
</entry>
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
<id>tag:shitposter.club,2018-02-23:noticeId=7387343:objectType=comment</id>
<title>New comment by shpuld</title>
<content type="html">@&lt;a href=&quot;https://gs.smuglo.li/user/589&quot; class=&quot;h-card mention&quot; title=&quot;&amp;#x16DE;&amp;#x16A9;&amp;#x16B3;&amp;#x16C1;&amp;#x16DE;&amp;#x16A9;&amp;#x16B3;&amp;#x16C1;&quot;&gt;dokidoki&lt;/a&gt; give them free upgrades to krokodil</content>
<link rel="alternate" type="text/html" href="https://shitposter.club/notice/7387343"/>
<status_net notice_id="7387343"></status_net>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<published>2018-02-23T12:53:15+00:00</published>
<updated>2018-02-23T12:53:15+00:00</updated>
<thr:in-reply-to ref="tag:gs.smuglo.li,2018-02-23:noticeId=6201061:objectType=note" href="https://gs.smuglo.li/notice/6201061"></thr:in-reply-to>
<link rel="related" href="https://gs.smuglo.li/notice/6201061"/>
<link rel="ostatus:conversation" href="https://shitposter.club/conversation/4390117"/>
<ostatus:conversation href="https://shitposter.club/conversation/4390117" local_id="4390117" ref="https://gs.smuglo.li/conversation/3934774">https://gs.smuglo.li/conversation/3934774</ostatus:conversation>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://gs.smuglo.li/user/589"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link rel="self" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387343.atom"/>
<link rel="edit" type="application/atom+xml" href="https://shitposter.club/api/statuses/show/7387343.atom"/>
<statusnet:notice_info local_id="7387343" source="Pleroma FE"></statusnet:notice_info>
</entry>
</feed>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Subject>https://shitposter.club/user/5381</Subject>
<Alias>acct:shpuld@shitposter.club</Alias>
<Alias>https://shitposter.club/shpuld</Alias>
<Alias>https://shitposter.club/index.php/user/5381</Alias>
<Alias>https://shitposter.club/index.php/shpuld</Alias>
<Link rel="http://webfinger.net/rel/profile-page" type="text/html" href="https://shitposter.club/shpuld"/>
<Link rel="http://gmpg.org/xfn/11" type="text/html" href="https://shitposter.club/shpuld"/>
<Link rel="describedby" type="application/rdf+xml" href="https://shitposter.club/shpuld/foaf"/>
<Link rel="http://apinamespace.org/atom" type="application/atomsvc+xml" href="https://shitposter.club/api/statusnet/app/service/shpuld.xml"/>
<Link rel="http://apinamespace.org/twitter" href="https://shitposter.club/api/"/>
<Link rel="http://specs.openid.net/auth/2.0/provider" href="https://shitposter.club/shpuld"/>
<Link rel="http://schemas.google.com/g/2010#updates-from" type="application/atom+xml" href="https://shitposter.club/api/statuses/user_timeline/5381.atom"/>
<Link rel="magic-public-key" href="data:application/magic-public-key,RSA.pkJ_xCKxFzcOKuKPKFhUTkWLWyWAIRDS8onxRLxVvxITQAkHIO1Rl9FS_1DAT3MK_wBcbzXm1TwlVOQFY5I2zrZQGxUvGDUlqcsf9sQyQaNvVVoU83nAV2w9bQZ-GlaLCMHWKN4yBBCTPfu9J6XbItxbHhJg5ub8z5drDF45te8=.AQAB"/>
<Link rel="salmon" href="https://shitposter.club/main/salmon/user/5381"/>
<Link rel="http://salmon-protocol.org/ns/salmon-replies" href="https://shitposter.club/main/salmon/user/5381"/>
<Link rel="http://salmon-protocol.org/ns/salmon-mention" href="https://shitposter.club/main/salmon/user/5381"/>
<Link rel="http://ostatus.org/schema/1.0/subscribe" template="https://shitposter.club/main/ostatussub?profile={uri}"/>
</XRD>

View File

@ -0,0 +1,34 @@
{
"type": "Accept",
"signature": {
"type": "RsaSignature2017",
"signatureValue": "rBzK4Kqhd4g7HDS8WE5oRbWQb2R+HF/6awbUuMWhgru/xCODT0SJWSri0qWqEO4fPcpoUyz2d25cw6o+iy9wiozQb3hQNnu69AR+H5Mytc06+g10KCHexbGhbAEAw/7IzmeXELHUbaqeduaDIbdt1zw4RkwLXdqgQcGXTJ6ND1wM3WMHXQCK1m0flasIXFoBxpliPAGiElV8s0+Ltuh562GvflG3kB3WO+j+NaR0ZfG5G9N88xMj9UQlCKit5gpAE5p6syUsCU2WGBHywTumv73i3OVTIFfq+P9AdMsRuzw1r7zoKEsthW4aOzLQDi01ZjvdBz8zH6JnjDU7SMN/Ig==",
"creator": "http://mastodon.example.org/users/admin#main-key",
"created": "2018-02-17T14:36:41Z"
},
"object": {
"type": "Follow",
"object": "http://mastodon.example.org/users/admin",
"id": "http://localtesting.pleroma.lol/users/lain#follows/4",
"actor": "http://localtesting.pleroma.lol/users/lain"
},
"nickname": "lain",
"id": "http://mastodon.example.org/users/admin#accepts/follows/4",
"actor": "http://mastodon.example.org/users/admin",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"toot": "http://joinmastodon.org/ns#",
"sensitive": "as:sensitive",
"ostatus": "http://ostatus.org#",
"movedTo": "as:movedTo",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"atomUri": "ostatus:atomUri",
"Hashtag": "as:Hashtag",
"Emoji": "toot:Emoji"
}
]
}

37
test/fixtures/mastodon-announce.json vendored Normal file
View File

@ -0,0 +1,37 @@
{
"type": "Announce",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"signature": {
"type": "RsaSignature2017",
"signatureValue": "T95DRE0eAligvMuRMkQA01lsoz2PKi4XXF+cyZ0BqbrO12p751TEWTyyRn5a+HH0e4kc77EUhQVXwMq80WAYDzHKVUTf2XBJPBa68vl0j6RXw3+HK4ef5hR4KWFNBU34yePS7S1fEmc1mTG4Yx926wtmZwDpEMTp1CXOeVEjCYzmdyHpepPPH2ZZettiacmPRSqBLPGWZoot7kH/SioIdnrMGY0I7b+rqkIdnnEcdhu9N1BKPEO9Sr+KmxgAUiidmNZlbBXX6gCxp8BiIdH4ABsIcwoDcGNkM5EmWunGW31LVjsEQXhH5c1Wly0ugYYPCg/0eHLNBOhKkY/teSM8Lg==",
"creator": "http://mastodon.example.org/users/admin#main-key",
"created": "2018-02-17T19:39:15Z"
},
"published": "2018-02-17T19:39:15Z",
"object": "http://mastodon.example.org/@admin/99541947525187367",
"id": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity",
"cc": [
"http://mastodon.example.org/users/admin",
"http://mastodon.example.org/users/admin/followers"
],
"atomUri": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity",
"actor": "http://mastodon.example.org/users/admin",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"toot": "http://joinmastodon.org/ns#",
"sensitive": "as:sensitive",
"ostatus": "http://ostatus.org#",
"movedTo": "as:movedTo",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"atomUri": "ostatus:atomUri",
"Hashtag": "as:Hashtag",
"Emoji": "toot:Emoji"
}
]
}

View File

@ -0,0 +1,63 @@
{
"type": "Create",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"signature": {
"type": "RsaSignature2017",
"signatureValue": "KnaBoP7C4XYgzTFbM+CpGlx4p59ahWvNNo4reRGDlb/DmxL3OF1/WugNl0xHCOA3aoIX2rrkHniw+z4Yb+wOBf9ZOxgM+IHTKj69AEcm/4NxGXxStRv603JZNyboY371w8g/mIKmLLtL6dgUI3n2Laam2rYh//8aelEWQ240TxiJi/WcKuOT2DNInWOpfArgxJ4MA11n4tb4xX65RkxInTCFa1kaJG8L+A+EoXtIhTa4rCQDv/BH3a8x7vOJxHfEosEnkk/yVEqG+ccgoTvc+5/kK+TKk3S3GuXch0ro9RKqxfPAHkyg8eetRhNhKWZ/rgPNfcF6bGJKFA0i8TzjHw==",
"creator": "http://mastodon.example.org/users/admin#main-key",
"created": "2018-02-17T17:14:26Z"
},
"published": "2018-02-17T17:14:26Z",
"object": {
"url": "http://mastodon.example.org/@admin/99541822081679796",
"type": "Note",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"tag": [],
"summary": null,
"sensitive": false,
"published": "2018-02-17T17:14:26Z",
"inReplyToAtomUri": null,
"inReplyTo": null,
"id": "http://mastodon.example.org/users/admin/statuses/99541822081679796",
"conversation": "tag:mastodon.example.org,2018-02-17:objectId=10:objectType=Conversation",
"content": "<p><a href=\"http://mastodon.example.org/media/hw4nrZmV5DPbW2z_hao\" rel=\"nofollow noopener\" target=\"_blank\"><span class=\"invisible\">http://</span><span class=\"ellipsis\">mastodon.example.org/media/hw4</span><span class=\"invisible\">nrZmV5DPbW2z_hao</span></a></p>",
"cc": [
"http://mastodon.example.org/users/admin/followers"
],
"attributedTo": "http://mastodon.example.org/users/admin",
"attachment": [
{
"url": "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
"type": "Document",
"name": null,
"mediaType": "image/jpeg"
}
],
"atomUri": "http://mastodon.example.org/users/admin/statuses/99541822081679796"
},
"id": "http://mastodon.example.org/users/admin/statuses/99541822081679796/activity",
"cc": [
"http://mastodon.example.org/users/admin/followers"
],
"actor": "http://mastodon.example.org/users/admin",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"toot": "http://joinmastodon.org/ns#",
"sensitive": "as:sensitive",
"ostatus": "http://ostatus.org#",
"movedTo": "as:movedTo",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"atomUri": "ostatus:atomUri",
"Hashtag": "as:Hashtag",
"Emoji": "toot:Emoji"
}
]
}

33
test/fixtures/mastodon-delete.json vendored Normal file
View File

@ -0,0 +1,33 @@
{
"type": "Delete",
"signature": {
"type": "RsaSignature2017",
"signatureValue": "cw0RlfNREf+5VdsOYcCBDrv521eiLsDTAYNHKffjF0bozhCnOh+wHkFik7WamUk$
uEiN4L2H6vPlGRprAZGRhEwgy+A7rIFQNmLrpW5qV5UNVI/2F7kngEHqZQgbQYj9hW+5GMYmPkHdv3D72ZefGw$
4Xa2NBLGFpAjQllfzt7kzZLKKY2DM99FdUa64I2Wj3iD04Hs23SbrUdAeuGk/c1Cg6bwGNG4vxoiwn1jikgJLA$
NAlSGjsRGdR7LfbC7GqWWsW3cSNsLFPoU6FyALjgTrrYoHiXe0QHggw+L3yMLfzB2S/L46/VRbyb+WDKMBIXUL$
5owmzHSi6e/ZtCI3w==",
"creator": "http://mastodon.example.org/users/gargron#main-key", "created": "2018-03-03T16:24:11Z"
},
"object": {
"type": "Tombstone",
"id": "http://mastodon.example.org/users/gargron/statuses/99620895606148759",
"atomUri": "http://mastodon.example.org/users/gargron/statuses/99620895606148759"
},
"id": "http://mastodon.example.org/users/gargron/statuses/99620895606148759#delete",
"actor": "http://mastodon.example.org/users/gargron",
"@context": [
{
"toot": "http://joinmastodon.org/ns#",
"sensitive": "as:sensitive",
"ostatus": "http://ostatus.org#",
"movedTo": "as:movedTo",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"atomUri": "ostatus:atomUri",
"Hashtag": "as:Hashtag",
"Emoji": "toot:Emoji"
}
]
}

View File

@ -0,0 +1,29 @@
{
"type": "Follow",
"signature": {
"type": "RsaSignature2017",
"signatureValue": "Kn1/UkAQGJVaXBfWLAHcnwHg8YMAUqlEaBuYLazAG+pz5hqivsyrBmPV186Xzr+B4ZLExA9+SnOoNx/GOz4hBm0kAmukNSILAsUd84tcJ2yT9zc1RKtembK4WiwOw7li0+maeDN0HaB6t+6eTqsCWmtiZpprhXD8V1GGT8yG7X24fQ9oFGn+ng7lasbcCC0988Y1eGqNe7KryxcPuQz57YkDapvtONzk8gyLTkZMV4De93MyRHq6GVjQVIgtiYabQAxrX6Q8C+4P/jQoqdWJHEe+MY5JKyNaT/hMPt2Md1ok9fZQBGHlErk22/zy8bSN19GdG09HmIysBUHRYpBLig==",
"creator": "http://mastodon.example.org/users/admin#main-key",
"created": "2018-02-17T13:29:31Z"
},
"object": "http://localtesting.pleroma.lol/users/lain",
"nickname": "lain",
"id": "http://mastodon.example.org/users/admin#follows/2",
"actor": "http://mastodon.example.org/users/admin",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"toot": "http://joinmastodon.org/ns#",
"sensitive": "as:sensitive",
"ostatus": "http://ostatus.org#",
"movedTo": "as:movedTo",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"atomUri": "ostatus:atomUri",
"Hashtag": "as:Hashtag",
"Emoji": "toot:Emoji"
}
]
}

29
test/fixtures/mastodon-like.json vendored Normal file
View File

@ -0,0 +1,29 @@
{
"type": "Like",
"signature": {
"type": "RsaSignature2017",
"signatureValue": "fdxMfQSMwbC6wP6sh6neS/vM5879K67yQkHTbiT5Npr5wAac0y6+o3Ij+41tN3rL6wfuGTosSBTHOtta6R4GCOOhCaCSLMZKypnp1VltCzLDoyrZELnYQIC8gpUXVmIycZbREk22qWUe/w7DAFaKK4UscBlHDzeDVcA0K3Se5Sluqi9/Zh+ldAnEzj/rSEPDjrtvf5wGNf3fHxbKSRKFt90JvKK6hS+vxKUhlRFDf6/SMETw+EhwJSNW4d10yMUakqUWsFv4Acq5LW7l+HpYMvlYY1FZhNde1+uonnCyuQDyvzkff8zwtEJmAXC4RivO/VVLa17SmqheJZfI8oluVg==",
"creator": "http://mastodon.example.org/users/admin#main-key",
"created": "2018-02-17T18:57:49Z"
},
"object": "http://localtesting.pleroma.lol/objects/eb92579d-3417-42a8-8652-2492c2d4f454",
"nickname": "lain",
"id": "http://mastodon.example.org/users/admin#likes/2",
"actor": "http://mastodon.example.org/users/admin",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"toot": "http://joinmastodon.org/ns#",
"sensitive": "as:sensitive",
"ostatus": "http://ostatus.org#",
"movedTo": "as:movedTo",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"atomUri": "ostatus:atomUri",
"Hashtag": "as:Hashtag",
"Emoji": "toot:Emoji"
}
]
}

View File

@ -0,0 +1,9 @@
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":"as:movedTo","Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji"}],"id":"http://mastodon.example.org/users/admin/statuses/99541947525187367","type":"Note","summary":null,"content":"\u003cp\u003eyeah.\u003c/p\u003e","inReplyTo":null,"published":"2018-02-17T17:46:20Z","url":"http://mastodon.example.org/@admin/99541947525187367","attributedTo":"http://mastodon.example.org/users/admin","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["http://mastodon.example.org/users/admin/followers"],"sensitive":false,"atomUri":"http://mastodon.example.org/users/admin/statuses/99541947525187367","inReplyToAtomUri":null,"conversation":"tag:mastodon.example.org,2018-02-17:objectId=59:objectType=Conversation","tag":[],
"attachment": [
{
"url": "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
"type": "Document",
"name": null,
"mediaType": "image/jpeg"
}
]}

View File

@ -0,0 +1,38 @@
<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:mastodon="http://mastodon.social/schema/1.0">
<id>https://mastodon.social/users/lambadalambda.atom</id>
<title>Critical Value</title>
<subtitle></subtitle>
<updated>2017-04-16T21:47:25Z</updated>
<logo>https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif</logo>
<author>
<id>https://mastodon.social/users/lambadalambda</id>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<uri>https://mastodon.social/users/lambadalambda</uri>
<name>lambadalambda</name>
<email>lambadalambda@mastodon.social</email>
<link rel="alternate" type="text/html" href="https://mastodon.social/@lambadalambda"/>
<link rel="avatar" type="image/gif" media:width="120" media:height="120" href="https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif"/>
<link rel="header" type="" media:width="700" media:height="335" href="/headers/original/missing.png"/>
<poco:preferredUsername>lambadalambda</poco:preferredUsername>
<poco:displayName>Critical Value</poco:displayName>
<mastodon:scope>public</mastodon:scope>
</author>
<link rel="alternate" type="text/html" href="https://mastodon.social/@lambadalambda"/>
<link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda.atom"/>
<link rel="hub" href="https://mastodon.social/api/push"/>
<link rel="salmon" href="https://mastodon.social/api/salmon/264"/>
<entry>
<id>tag:mastodon.social,2017-05-10:objectId=5551985:objectType=Status</id>
<published>2017-05-10T12:21:36Z</published>
<updated>2017-05-10T12:21:36Z</updated>
<title>New status by lambadalambda</title>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<summary xml:lang="sv">technologic</summary>
<content type="html" xml:lang="sv">&lt;p&gt;test&lt;/p&gt;</content>
<mastodon:scope>unlisted</mastodon:scope>
<link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/2314748"/>
<link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/2314748.atom"/>
</entry>
</feed>

View File

@ -0,0 +1,65 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"Emoji": "toot:Emoji",
"Hashtag": "as:Hashtag",
"atomUri": "ostatus:atomUri",
"conversation": "ostatus:conversation",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"movedTo": "as:movedTo",
"ostatus": "http://ostatus.org#",
"sensitive": "as:sensitive",
"toot": "http://joinmastodon.org/ns#"
}
],
"actor": "http://mastodon.example.org/users/admin",
"cc": [
"http://mastodon.example.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain"
],
"id": "http://mastodon.example.org/users/admin/statuses/99512778738411822/activity",
"nickname": "lain",
"object": {
"atomUri": "http://mastodon.example.org/users/admin/statuses/99512778738411822",
"attachment": [],
"attributedTo": "http://mastodon.example.org/users/admin",
"cc": [
"http://mastodon.example.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain"
],
"content": "<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>",
"conversation": "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation",
"id": "http://mastodon.example.org/users/admin/statuses/99512778738411822",
"inReplyTo": null,
"inReplyToAtomUri": null,
"published": "2018-02-12T14:08:20Z",
"sensitive": true,
"summary": "cw",
"tag": [
{
"href": "http://localtesting.pleroma.lol/users/lain",
"name": "@lain@localtesting.pleroma.lol",
"type": "Mention"
}
],
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Note",
"url": "http://mastodon.example.org/@admin/99512778738411822"
},
"published": "2018-02-12T14:08:20Z",
"signature": {
"created": "2018-02-12T14:08:20Z",
"creator": "http://mastodon.example.org/users/admin#main-key",
"signatureValue": "rnNfcopkc6+Ju73P806popcfwrK9wGYHaJVG1/ZvrlEbWVDzaHjkXqj9Q3/xju5l8CSn9tvSgCCtPFqZsFQwn/pFIFUcw7ZWB2xi4bDm3NZ3S4XQ8JRaaX7og5hFxAhWkGhJhAkfxVnOg2hG+w2d/7d7vRVSC1vo5ip4erUaA/PkWusZvPIpxnRWoXaxJsFmVx0gJgjpJkYDyjaXUlp+jmaoseeZ4EPQUWqHLKJ59PRG0mg8j2xAjYH9nQaN14qMRmTGPxY8gfv/CUFcatA+8VJU9KEsJkDAwLVvglydNTLGrxpAJU78a2eaht0foV43XUIZGe3DKiJPgE+UOKGCJw==",
"type": "RsaSignature2017"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Create"
}

43
test/fixtures/mastodon-update.json vendored Normal file
View File

@ -0,0 +1,43 @@
{
"type": "Update",
"object": {
"url": "http://mastodon.example.org/@gargron",
"type": "Person",
"summary": "<p>Some bio</p>",
"publicKey": {
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0gs3VnQf6am3R+CeBV4H\nlfI1HZTNRIBHgvFszRZkCERbRgEWMu+P+I6/7GJC5H5jhVQ60z4MmXcyHOGmYMK/\n5XyuHQz7V2Ssu1AxLfRN5Biq1ayb0+DT/E7QxNXDJPqSTnstZ6C7zKH/uAETqg3l\nBonjCQWyds+IYbQYxf5Sp3yhvQ80lMwHML3DaNCMlXWLoOnrOX5/yK5+dedesg2\n/HIvGk+HEt36vm6hoH7bwPuEkgA++ACqwjXRe5Mta7i3eilHxFaF8XIrJFARV0t\nqOu4GID/jG6oA+swIWndGrtR2QRJIt9QIBFfK3HG5M0koZbY1eTqwNFRHFL3xaD\nUQIDAQAB\n-----END PUBLIC KEY-----\n",
"owner": "http://mastodon.example.org/users/gargron",
"id": "http://mastodon.example.org/users/gargron#main-key"
},
"preferredUsername": "gargron",
"outbox": "http://mastodon.example.org/users/gargron/outbox",
"name": "gargle",
"manuallyApprovesFollowers": false,
"inbox": "http://mastodon.example.org/users/gargron/inbox",
"id": "http://mastodon.example.org/users/gargron",
"following": "http://mastodon.example.org/users/gargron/following",
"followers": "http://mastodon.example.org/users/gargron/followers",
"endpoints": {
"sharedInbox": "http://mastodon.example.org/inbox"
},
"icon":{"type":"Image","mediaType":"image/jpeg","url":"https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"},"image":{"type":"Image","mediaType":"image/png","url":"https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"}
},
"id": "http://mastodon.example.org/users/gargron#updates/1519563538",
"actor": "http://mastodon.example.org/users/gargron",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"toot": "http://joinmastodon.org/ns#",
"sensitive": "as:sensitive",
"ostatus": "http://ostatus.org#",
"movedTo": "as:movedTo",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"atomUri": "ostatus:atomUri",
"Hashtag": "as:Hashtag",
"Emoji": "toot:Emoji"
}
]
}

View File

@ -8,9 +8,11 @@ def build(data \\ %{}, opts \\ %{}) do
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id, "id" => Pleroma.Web.ActivityPub.Utils.generate_object_id,
"actor" => user.ap_id, "actor" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"], "to" => ["https://www.w3.org/ns/activitystreams#Public"],
"type" => "Create",
"object" => %{ "object" => %{
"type" => "Note", "type" => "Note",
"content" => "test" "content" => "test",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
} }
} }
Map.merge(activity, data) Map.merge(activity, data)
@ -23,7 +25,7 @@ def insert(data \\ %{}, opts \\ %{}) do
def insert_list(times, data \\ %{}, opts \\ %{}) do def insert_list(times, data \\ %{}, opts \\ %{}) do
Enum.map(1..times, fn (n) -> Enum.map(1..times, fn (n) ->
{:ok, activity} = insert(data) {:ok, activity} = insert(data, opts)
activity activity
end) end)
end end
@ -32,7 +34,7 @@ def public_and_non_public do
user = Pleroma.Factory.insert(:user) user = Pleroma.Factory.insert(:user)
public = build(%{"id" => 1}, %{user: user}) public = build(%{"id" => 1}, %{user: user})
non_public = build(%{"id" => 2, "to" => []}, %{user: user}) non_public = build(%{"id" => 2, "to" => [user.follower_address]}, %{user: user})
{:ok, public} = ActivityPub.insert(public) {:ok, public} = ActivityPub.insert(public)
{:ok, non_public} = ActivityPub.insert(non_public) {:ok, non_public} = ActivityPub.insert(non_public)

View File

@ -14,6 +14,8 @@ def build(data \\ %{}) do
end end
def insert(data \\ %{}) do def insert(data \\ %{}) do
Repo.insert(build(data)) {:ok, user} = Repo.insert(build(data))
User.invalidate_cache(user)
{:ok, user}
end end
end end

View File

@ -52,7 +52,8 @@ def note_activity_factory do
%Pleroma.Activity{ %Pleroma.Activity{
data: data, data: data,
actor: data["actor"] actor: data["actor"],
recipients: data["to"]
} }
end end

View File

@ -80,6 +80,13 @@ def get("https://shitposter.club/.well-known/webfinger?resource=https://shitpost
}} }}
end end
def get("https://shitposter.club/.well-known/webfinger?resource=https://shitposter.club/user/5381", [Accept: "application/xrd+xml"], []) do
{:ok, %Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/spc_5381_xrd.xml")
}}
end
def get("http://gs.example.org/.well-known/webfinger", [Accept: "application/xrd+xml"], [params: [resource: "http://gs.example.org:4040/index.php/user/1"], follow_redirect: true]) do def get("http://gs.example.org/.well-known/webfinger", [Accept: "application/xrd+xml"], [params: [resource: "http://gs.example.org:4040/index.php/user/1"], follow_redirect: true]) do
{:ok, %Response{ {:ok, %Response{
status_code: 200, status_code: 200,
@ -122,6 +129,13 @@ def get("https://social.heldscal.la/api/statuses/user_timeline/29191.atom", _bod
}} }}
end end
def get("https://shitposter.club/api/statuses/user_timeline/5381.atom", _body, _headers) do
{:ok, %Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/spc_5381.atom")
}}
end
def get("https://social.heldscal.la/api/statuses/user_timeline/23211.atom", _body, _headers) do def get("https://social.heldscal.la/api/statuses/user_timeline/23211.atom", _body, _headers) do
{:ok, %Response{ {:ok, %Response{
status_code: 200, status_code: 200,
@ -366,6 +380,62 @@ def get("http://social.stopwatchingus-heidelberg.de/.well-known/host-meta", [],
}} }}
end end
def get("http://mastodon.example.org/users/admin", ["Accept": "application/activity+json"], _) do
{:ok, %Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/admin@mastdon.example.org.json")
}}
end
def get("https://masto.quad.moe/users/_HellPie", ["Accept": "application/activity+json"], _) do
{:ok, %Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/hellpie.json")
}}
end
def get("https://niu.moe/users/rye", ["Accept": "application/activity+json"], _) do
{:ok, %Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/rye.json")
}}
end
def get("https://mstdn.io/users/mayuutann", ["Accept": "application/activity+json"], _) do
{:ok, %Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/mayumayu.json")
}}
end
def get("http://mastodon.example.org/@admin/99541947525187367", ["Accept": "application/activity+json"], _) do
{:ok, %Response{
status_code: 200,
body: File.read!("test/fixtures/mastodon-note-object.json")
}}
end
def get("https://mstdn.io/users/mayuutann/statuses/99568293732299394", ["Accept": "application/activity+json"], _) do
{:ok, %Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/mayumayupost.json")
}}
end
def get("https://shitposter.club/notice/7369654", _, _) do
{:ok, %Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/7369654.html")
}}
end
def get("https://shitposter.club/api/statuses/show/7369654.atom", _body, _headers) do
{:ok, %Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/7369654.atom")
}}
end
def get(url, body, headers) do def get(url, body, headers) do
{:error, "Not implemented the mock response for get #{inspect(url)}, #{inspect(body)}, #{inspect(headers)}"} {:error, "Not implemented the mock response for get #{inspect(url)}, #{inspect(body)}, #{inspect(headers)}"}
end end

View File

@ -46,21 +46,22 @@ test "can't follow a deactivated users" do
{:error, _} = User.follow(user, followed) {:error, _} = User.follow(user, followed)
end end
test "following a remote user will ensure a websub subscription is present" do # This is a somewhat useless test.
user = insert(:user) # test "following a remote user will ensure a websub subscription is present" do
{:ok, followed} = OStatus.make_user("shp@social.heldscal.la") # user = insert(:user)
# {:ok, followed} = OStatus.make_user("shp@social.heldscal.la")
assert followed.local == false # assert followed.local == false
{:ok, user} = User.follow(user, followed) # {:ok, user} = User.follow(user, followed)
assert User.ap_followers(followed) in user.following # assert User.ap_followers(followed) in user.following
query = from w in WebsubClientSubscription, # query = from w in WebsubClientSubscription,
where: w.topic == ^followed.info["topic"] # where: w.topic == ^followed.info["topic"]
websub = Repo.one(query) # websub = Repo.one(query)
assert websub # assert websub
end # end
test "unfollow takes a user and another user" do test "unfollow takes a user and another user" do
followed = insert(:user) followed = insert(:user)
@ -371,4 +372,15 @@ test ".delete deactivates a user, all follow relationships and all create activi
refute Repo.get(Activity, activity.id) refute Repo.get(Activity, activity.id)
end end
test "get_public_key_for_ap_id fetches a user that's not in the db" do
assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin")
end
test "insert or update a user from given data" do
user = insert(:user, %{nickname: "nick@name.de"})
data = %{ ap_id: user.ap_id <> "xxx", name: user.name, nickname: user.nickname }
assert {:ok, %User{}} = User.insert_or_update_user(data)
end
end end

View File

@ -0,0 +1,49 @@
defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
alias Pleroma.Web.ActivityPub.{UserView, ObjectView}
alias Pleroma.{Repo, User}
alias Pleroma.Activity
describe "/users/:nickname" do
test "it returns a json representation of the user", %{conn: conn} do
user = insert(:user)
conn = conn
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}")
user = Repo.get(User, user.id)
assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
end
end
describe "/object/:uuid" do
test "it returns a json representation of the object", %{conn: conn} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last
conn = conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
end
end
describe "/users/:nickname/inbox" do
test "it inserts an incoming activity into the database", %{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!
conn = conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
:timer.sleep(500)
assert Activity.get_by_ap_id(data["id"])
end
end
end

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,277 @@
defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
use Pleroma.DataCase
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Repo
alias Pleroma.Web.Websub.WebsubClientSubscription
alias Pleroma.Web.Websub.WebsubServerSubscription
import Ecto.Query
import Pleroma.Factory
alias Pleroma.Web.CommonAPI
describe "handle_incoming" do
test "it ignores an incoming notice if we already have it" do
activity = insert(:note_activity)
data = File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!
|> Map.put("object", activity.data["object"])
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
assert activity == returned_activity
end
test "it fetches replied-to activities if we don't have them" do
data = File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!
object = data["object"]
|> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
data = data
|> Map.put("object", object)
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
assert activity = Activity.get_create_activity_by_object_ap_id("tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment")
assert returned_activity.data["object"]["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
assert returned_activity.data["object"]["inReplyToStatusId"] == activity.id
end
test "it works for incoming notices" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["id"] == "http://mastodon.example.org/users/admin/statuses/99512778738411822/activity"
assert data["context"] == "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
assert data["cc"] == [
"http://mastodon.example.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain"
]
assert data["actor"] == "http://mastodon.example.org/users/admin"
object = data["object"]
assert object["id"] == "http://mastodon.example.org/users/admin/statuses/99512778738411822"
assert object["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
assert object["cc"] == [
"http://mastodon.example.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain"
]
assert object["actor"] == "http://mastodon.example.org/users/admin"
assert object["attributedTo"] == "http://mastodon.example.org/users/admin"
assert object["context"] == "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
assert object["sensitive"] == true
end
test "it works for incoming follow requests" do
user = insert(:user)
data = File.read!("test/fixtures/mastodon-follow-activity.json") |> Poison.decode!
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Follow"
assert data["id"] == "http://mastodon.example.org/users/admin#follows/2"
assert User.following?(User.get_by_ap_id(data["actor"]), user)
end
test "it works for incoming likes" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data = File.read!("test/fixtures/mastodon-like.json") |> Poison.decode!
|> Map.put("object", activity.data["object"]["id"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Like"
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2"
assert data["object"] == activity.data["object"]["id"]
end
test "it works for incoming announces" do
data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Announce"
assert data["id"] == "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
assert data["object"] == "http://mastodon.example.org/users/admin/statuses/99541947525187367"
assert Activity.get_create_activity_by_object_ap_id(data["object"])
end
test "it works for incoming announces with an existing activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
data = File.read!("test/fixtures/mastodon-announce.json")
|> Poison.decode!
|> Map.put("object", activity.data["object"]["id"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Announce"
assert data["id"] == "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
assert data["object"] == activity.data["object"]["id"]
assert Activity.get_create_activity_by_object_ap_id(data["object"]).id == activity.id
end
test "it works for incoming update activities" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!
object = update_data["object"]
|> Map.put("actor", data["actor"])
|> Map.put("id", data["actor"])
update_data = update_data
|> Map.put("actor", data["actor"])
|> Map.put("object", object)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
user = User.get_cached_by_ap_id(data["actor"])
assert user.name == "gargle"
assert user.avatar["url"] == [%{"href" => "https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"}]
assert user.info["banner"]["url"] == [%{"href" => "https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"}]
assert user.bio == "<p>Some bio</p>"
end
test "it works for incoming deletes" do
activity = insert(:note_activity)
data = File.read!("test/fixtures/mastodon-delete.json")
|> Poison.decode!
object = data["object"]
|> Map.put("id", activity.data["object"]["id"])
data = data
|> Map.put("object", object)
|> Map.put("actor", activity.data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
refute Repo.get(Activity, activity.id)
end
end
describe "prepare outgoing" do
test "it turns mentions into tags" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey, @#{other_user.nickname}, how are ya? #2hu"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
object = modified["object"]
expected_mention = %{
"href" => other_user.ap_id,
"name" => "@#{other_user.nickname}",
"type" => "Mention"
}
expected_tag = %{
"href" => Pleroma.Web.Endpoint.url <> "/tags/2hu",
"type" => "Hashtag",
"name" => "#2hu"
}
assert Enum.member?(object["tag"], expected_tag)
assert Enum.member?(object["tag"], expected_mention)
end
test "it adds the sensitive property" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#nsfw hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["object"]["sensitive"]
end
test "it adds the json-ld context and the conversation property" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["@context"] == "https://www.w3.org/ns/activitystreams"
assert modified["object"]["conversation"] == modified["context"]
end
test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
end
end
describe "user upgrade" do
test "it upgrades a user to activitypub" do
user = insert(:user, %{nickname: "rye@niu.moe", local: false, ap_id: "https://niu.moe/users/rye", follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})})
user_two = insert(:user, %{following: [user.follower_address]})
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
{:ok, unrelated_activity} = CommonAPI.post(user_two, %{"status" => "test"})
assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients
user = Repo.get(User, user.id)
assert user.info["note_count"] == 1
{:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye")
assert user.info["ap_enabled"]
assert user.info["note_count"] == 1
assert user.follower_address == "https://niu.moe/users/rye/followers"
# Wait for the background task
:timer.sleep(1000)
user = Repo.get(User, user.id)
assert user.info["note_count"] == 1
activity = Repo.get(Activity, activity.id)
assert user.follower_address in activity.recipients
assert %{"url" => [%{"href" => "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"}]} = user.avatar
assert %{"url" => [%{"href" => "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"}]} = user.info["banner"]
refute "..." in activity.recipients
unrelated_activity = Repo.get(Activity, unrelated_activity.id)
refute user.follower_address in unrelated_activity.recipients
user_two = Repo.get(User, user_two.id)
assert user.follower_address in user_two.following
refute "..." in user_two.following
end
end
describe "maybe_retire_websub" do
test "it deletes all websub client subscripitions with the user as topic" do
subscription = %WebsubClientSubscription{topic: "https://niu.moe/users/rye.atom"}
{:ok, ws} = Repo.insert(subscription)
subscription = %WebsubClientSubscription{topic: "https://niu.moe/users/pasty.atom"}
{:ok, ws2} = Repo.insert(subscription)
Transmogrifier.maybe_retire_websub("https://niu.moe/users/rye")
refute Repo.get(WebsubClientSubscription, ws.id)
assert Repo.get(WebsubClientSubscription, ws2.id)
end
end
end

View File

@ -0,0 +1,17 @@
defmodule Pleroma.Web.ActivityPub.ObjectViewTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Web.ActivityPub.ObjectView
test "renders a note object" do
note = insert(:note)
result = ObjectView.render("object.json", %{object: note})
assert result["id"] == note.data["id"]
assert result["to"] == note.data["to"]
assert result["content"] == note.data["content"]
assert result["type"] == "Note"
end
end

View File

@ -0,0 +1,18 @@
defmodule Pleroma.Web.ActivityPub.UserViewTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Web.ActivityPub.UserView
test "Renders a user, including the public key" do
user = insert(:user)
{:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user})
assert result["id"] == user.ap_id
assert result["preferredUsername"] == user.nickname
assert String.contains?(result["publicKey"]["publicKeyPem"], "BEGIN RSA PUBLIC KEY")
end
end

View File

@ -0,0 +1,154 @@
# http signatures
# Test data from https://tools.ietf.org/html/draft-cavage-http-signatures-08#appendix-C
defmodule Pleroma.Web.HTTPSignaturesTest do
use Pleroma.DataCase
alias Pleroma.Web.HTTPSignatures
import Pleroma.Factory
@private_key (hd(:public_key.pem_decode(File.read!("test/web/http_sigs/priv.key")))
|> :public_key.pem_entry_decode())
@public_key (hd(:public_key.pem_decode(File.read!("test/web/http_sigs/pub.key")))
|> :public_key.pem_entry_decode())
@headers %{
"(request-target)" => "post /foo?param=value&pet=dog",
"host" => "example.com",
"date" => "Thu, 05 Jan 2014 21:31:40 GMT",
"content-type" => "application/json",
"digest" => "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=",
"content-length" => "18"
}
@body "{\"hello\": \"world\"}"
@default_signature """
keyId="Test",algorithm="rsa-sha256",signature="jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w="
"""
@basic_signature """
keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date",signature="HUxc9BS3P/kPhSmJo+0pQ4IsCo007vkv6bUm4Qehrx+B1Eo4Mq5/6KylET72ZpMUS80XvjlOPjKzxfeTQj4DiKbAzwJAb4HX3qX6obQTa00/qPDXlMepD2JtTw33yNnm/0xV7fQuvILN/ys+378Ysi082+4xBQFwvhNvSoVsGv4="
"""
@all_headers_signature """
keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length",signature="Ef7MlxLXoBovhil3AlyjtBwAL9g4TN3tibLj7uuNB3CROat/9KaeQ4hW2NiJ+pZ6HQEOx9vYZAyi+7cmIkmJszJCut5kQLAwuX+Ms/mUFvpKlSo9StS2bMXDBNjOh4Auj774GFj4gwjS+3NhFeoqyr/MuN6HsEnkvn6zdgfE2i0="
"""
test "split up a signature" do
expected = %{
"keyId" => "Test",
"algorithm" => "rsa-sha256",
"signature" => "jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w=",
"headers" => ["date"]
}
assert HTTPSignatures.split_signature(@default_signature) == expected
end
test "validates the default case" do
signature = HTTPSignatures.split_signature(@default_signature)
assert HTTPSignatures.validate(@headers, signature, @public_key)
end
test "validates the basic case" do
signature = HTTPSignatures.split_signature(@basic_signature)
assert HTTPSignatures.validate(@headers, signature, @public_key)
end
test "validates the all-headers case" do
signature = HTTPSignatures.split_signature(@all_headers_signature)
assert HTTPSignatures.validate(@headers, signature, @public_key)
end
test "it contructs a signing string" do
expected = "date: Thu, 05 Jan 2014 21:31:40 GMT\ncontent-length: 18"
assert expected == HTTPSignatures.build_signing_string(@headers, ["date", "content-length"])
end
test "it validates a conn" do
public_key_pem = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnGb42rPZIapY4Hfhxrgn\nxKVJczBkfDviCrrYaYjfGxawSw93dWTUlenCVTymJo8meBlFgIQ70ar4rUbzl6GX\nMYvRdku072d1WpglNHXkjKPkXQgngFDrh2sGKtNB/cEtJcAPRO8OiCgPFqRtMiNM\nc8VdPfPdZuHEIZsJ/aUM38EnqHi9YnVDQik2xxDe3wPghOhqjxUM6eLC9jrjI+7i\naIaEygUdyst9qVg8e2FGQlwAeS2Eh8ygCxn+bBlT5OyV59jSzbYfbhtF2qnWHtZy\nkL7KOOwhIfGs7O9SoR2ZVpTEQ4HthNzainIe/6iCR5HGrao/T8dygweXFYRv+k5A\nPQIDAQAB\n-----END PUBLIC KEY-----\n"
[public_key] = :public_key.pem_decode(public_key_pem)
public_key = public_key
|> :public_key.pem_entry_decode()
conn = %{
req_headers: [
{"host", "localtesting.pleroma.lol"},
{"connection", "close"},
{"content-length", "2316"},
{"user-agent", "http.rb/2.2.2 (Mastodon/2.1.0.rc3; +http://mastodon.example.org/)"},
{"date", "Sun, 10 Dec 2017 14:23:49 GMT"},
{"digest", "SHA-256=x/bHADMW8qRrq2NdPb5P9fl0lYpKXXpe5h5maCIL0nM="},
{"content-type", "application/activity+json"},
{"(request-target)", "post /users/demiurge/inbox"},
{"signature", "keyId=\"http://mastodon.example.org/users/admin#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"i0FQvr51sj9BoWAKydySUAO1RDxZmNY6g7M62IA7VesbRSdFZZj9/fZapLp6YSuvxUF0h80ZcBEq9GzUDY3Chi9lx6yjpUAS2eKb+Am/hY3aswhnAfYd6FmIdEHzsMrpdKIRqO+rpQ2tR05LwiGEHJPGS0p528NvyVxrxMT5H5yZS5RnxY5X2HmTKEgKYYcvujdv7JWvsfH88xeRS7Jlq5aDZkmXvqoR4wFyfgnwJMPLel8P/BUbn8BcXglH/cunR0LUP7sflTxEz+Rv5qg+9yB8zgBsB4C0233WpcJxjeD6Dkq0EcoJObBR56F8dcb7NQtUDu7x6xxzcgSd7dHm5w==\""}]
}
assert HTTPSignatures.validate_conn(conn, public_key)
end
test "it validates a conn and fetches the key" do
conn = %{
params: %{"actor" => "http://mastodon.example.org/users/admin"},
req_headers: [
{"host", "localtesting.pleroma.lol"},
{"x-forwarded-for", "127.0.0.1"},
{"connection", "close"},
{"content-length", "2307"},
{"user-agent", "http.rb/2.2.2 (Mastodon/2.1.0.rc3; +http://mastodon.example.org/)"},
{"date", "Sun, 11 Feb 2018 17:12:01 GMT"},
{"digest", "SHA-256=UXsAnMtR9c7mi1FOf6HRMtPgGI1yi2e9nqB/j4rZ99I="},
{"content-type", "application/activity+json"},
{"signature", "keyId=\"http://mastodon.example.org/users/admin#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"qXKqpQXUpC3d9bZi2ioEeAqP8nRMD021CzH1h6/w+LRk4Hj31ARJHDwQM+QwHltwaLDUepshMfz2WHSXAoLmzWtvv7xRwY+mRqe+NGk1GhxVZ/LSrO/Vp7rYfDpfdVtkn36LU7/Bzwxvvaa4ZWYltbFsRBL0oUrqsfmJFswNCQIG01BB52BAhGSCORHKtQyzo1IZHdxl8y80pzp/+FOK2SmHkqWkP9QbaU1qTZzckL01+7M5btMW48xs9zurEqC2sM5gdWMQSZyL6isTV5tmkTZrY8gUFPBJQZgihK44v3qgfWojYaOwM8ATpiv7NG8wKN/IX7clDLRMA8xqKRCOKw==\""},
{"(request-target)", "post /users/demiurge/inbox"}
]
}
assert HTTPSignatures.validate_conn(conn)
end
test "validate this" do
conn = %{
params: %{"actor" => "https://niu.moe/users/rye"},
req_headers: [
{"x-forwarded-for", "149.202.73.191"},
{"host", "testing.pleroma.lol"},
{"x-cluster-client-ip", "149.202.73.191"},
{"connection", "upgrade"},
{"content-length", "2396"},
{"user-agent", "http.rb/3.0.0 (Mastodon/2.2.0; +https://niu.moe/)"},
{"date", "Sun, 18 Feb 2018 20:31:51 GMT"},
{"digest", "SHA-256=dzH+vLyhxxALoe9RJdMl4hbEV9bGAZnSfddHQzeidTU="},
{"content-type", "application/activity+json"},
{"signature", "keyId=\"https://niu.moe/users/rye#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"wtxDg4kIpW7nsnUcVJhBk6SgJeDZOocr8yjsnpDRqE52lR47SH6X7G16r7L1AUJdlnbfx7oqcvomoIJoHB3ghP6kRnZW6MyTMZ2jPoi3g0iC5RDqv6oAmDSO14iw6U+cqZbb3P/odS5LkbThF0UNXcfenVNfsKosIJycFjhNQc54IPCDXYq/7SArEKJp8XwEgzmiC2MdxlkVIUSTQYfjM4EG533cwlZocw1mw72e5mm/owTa80BUZAr0OOuhoWARJV9btMb02ZyAF6SCSoGPTA37wHyfM1Dk88NHf7Z0Aov/Fl65dpRM+XyoxdkpkrhDfH9qAx4iuV2VEWddQDiXHA==\""},
{"(request-target)", "post /inbox"}
]
}
assert HTTPSignatures.validate_conn(conn)
end
test "validate this too" do
conn = %{
params: %{"actor" => "https://niu.moe/users/rye"},
req_headers: [
{"x-forwarded-for", "149.202.73.191"},
{"host", "testing.pleroma.lol"},
{"x-cluster-client-ip", "149.202.73.191"},
{"connection", "upgrade"},
{"content-length", "2342"},
{"user-agent", "http.rb/3.0.0 (Mastodon/2.2.0; +https://niu.moe/)"},
{"date", "Sun, 18 Feb 2018 21:44:46 GMT"},
{"digest", "SHA-256=vS8uDOJlyAu78cF3k5EzrvaU9iilHCX3chP37gs5sS8="},
{"content-type", "application/activity+json"},
{"signature", "keyId=\"https://niu.moe/users/rye#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"IN6fHD8pLiDEf35dOaRHzJKc1wBYh3/Yq0ItaNGxUSbJTd2xMjigZbcsVKzvgYYjglDDN+disGNeD+OBKwMqkXWaWe/lyMc9wHvCH5NMhpn/A7qGLY8yToSt4vh8ytSkZKO6B97yC+Nvy6Fz/yMbvKtFycIvSXCq417cMmY6f/aG+rtMUlTbKO5gXzC7SUgGJCtBPCh1xZzu5/w0pdqdjO46ePNeR6JyJSLLV4hfo3+p2n7SRraxM4ePVCUZqhwS9LPt3Zdhy3ut+IXCZgMVIZggQFM+zXLtcXY5HgFCsFQr5WQDu+YkhWciNWtKFnWfAsnsg5sC330lZ/0Z8Z91yA==\""},
{"(request-target)", "post /inbox"}
]}
assert HTTPSignatures.validate_conn(conn)
end
test "it generates a signature" do
user = insert(:user)
assert HTTPSignatures.sign(user, %{host: "mastodon.example.org"}) =~ "keyId=\""
end
end

View File

@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF
NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F
UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB
AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA
QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK
kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg
f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u
412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc
mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7
kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA
gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW
G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA==
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3
6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6
Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw
oYi+1hqp1fIekaxsyQIDAQAB
-----END PUBLIC KEY-----

View File

@ -121,6 +121,7 @@ test "an announce activity" do
#{note_xml} #{note_xml}
</activity:object> </activity:object>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{note.data["actor"]}"/> <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{note.data["actor"]}"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
""" """
announce_xml = ActivityRepresenter.to_simple_form(announce, user) announce_xml = ActivityRepresenter.to_simple_form(announce, user)
@ -156,6 +157,7 @@ test "a like activity" do
<link rel="self" type="application/atom+xml" href="#{like.data["id"]}"/> <link rel="self" type="application/atom+xml" href="#{like.data["id"]}"/>
<thr:in-reply-to ref="#{note.data["id"]}" /> <thr:in-reply-to ref="#{note.data["id"]}" />
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{note.data["actor"]}"/> <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{note.data["actor"]}"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
""" """
assert clean(res) == clean(expected) assert clean(res) == clean(expected)

View File

@ -43,7 +43,7 @@ test "gets a feed", %{conn: conn} do
conn = conn conn = conn
|> get("/users/#{user.nickname}/feed.atom") |> get("/users/#{user.nickname}/feed.atom")
assert response(conn, 200) assert response(conn, 200) =~ note_activity.data["object"]["content"]
end end
test "gets an object", %{conn: conn} do test "gets an object", %{conn: conn} do

View File

@ -90,6 +90,15 @@ test "handle incoming notes - Mastodon, with CW" do
assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"] assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
end end
test "handle incoming unlisted messages, put public into cc" do
incoming = File.read!("test/fixtures/mastodon-note-unlisted.xml")
{:ok, [activity]} = OStatus.handle_incoming(incoming)
refute "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["cc"]
refute "https://www.w3.org/ns/activitystreams#Public" in activity.data["object"]["to"]
assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["object"]["cc"]
end
test "handle incoming retweets - Mastodon, with CW" do test "handle incoming retweets - Mastodon, with CW" do
incoming = File.read!("test/fixtures/cw_retweet.xml") incoming = File.read!("test/fixtures/cw_retweet.xml")
{:ok, [[_activity, retweeted_activity]]} = OStatus.handle_incoming(incoming) {:ok, [[_activity, retweeted_activity]]} = OStatus.handle_incoming(incoming)
@ -306,7 +315,8 @@ test "it returns user info in a hash" do
"fqn" => user, "fqn" => user,
"bio" => "cofe", "bio" => "cofe",
"avatar" => %{"type" => "Image", "url" => [%{"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", "mediaType" => "image/jpeg", "type" => "Link"}]}, "avatar" => %{"type" => "Image", "url" => [%{"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", "mediaType" => "image/jpeg", "type" => "Link"}]},
"subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}" "subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}",
"ap_id" => nil
} }
assert data == expected assert data == expected
end end
@ -330,7 +340,8 @@ test "it works with the uri" do
"fqn" => user, "fqn" => user,
"bio" => "cofe", "bio" => "cofe",
"avatar" => %{"type" => "Image", "url" => [%{"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", "mediaType" => "image/jpeg", "type" => "Link"}]}, "avatar" => %{"type" => "Image", "url" => [%{"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", "mediaType" => "image/jpeg", "type" => "Link"}]},
"subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}" "subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}",
"ap_id" => nil
} }
assert data == expected assert data == expected
end end
@ -355,13 +366,6 @@ test "it works for atom notes, too" do
end end
end end
test "insert or update a user from given data" do
user = insert(:user, %{nickname: "nick@name.de"})
data = %{ ap_id: user.ap_id <> "xxx", name: user.name, nickname: user.nickname }
assert {:ok, %User{}} = OStatus.insert_or_update_user(data)
end
test "it doesn't add nil in the do field" do test "it doesn't add nil in the do field" do
incoming = File.read!("test/fixtures/nil_mention_entry.xml") incoming = File.read!("test/fixtures/nil_mention_entry.xml")
{:ok, [activity]} = OStatus.handle_incoming(incoming) {:ok, [activity]} = OStatus.handle_incoming(incoming)

View File

@ -22,6 +22,7 @@ test "returns a user with id, uri, name and link" do
<name>#{user.nickname}</name> <name>#{user.nickname}</name>
<link rel="avatar" href="#{User.avatar_url(user)}" /> <link rel="avatar" href="#{User.avatar_url(user)}" />
<link rel="header" href="#{User.banner_url(user)}" /> <link rel="header" href="#{User.banner_url(user)}" />
<ap_enabled>true</ap_enabled>
""" """
assert clean(res) == clean(expected) assert clean(res) == clean(expected)

View File

@ -59,7 +59,6 @@ test "encodes an xml payload with a private key" do
end end
test "it gets a magic key" do test "it gets a magic key" do
# TODO: Make test local
salmon = File.read!("test/fixtures/salmon2.xml") salmon = File.read!("test/fixtures/salmon2.xml")
{:ok, key} = Salmon.fetch_magic_key(salmon) {:ok, key} = Salmon.fetch_magic_key(salmon)
@ -86,7 +85,7 @@ test "it pushes an activity to remote accounts it's addressed to" do
"context" => note.data["context"] "context" => note.data["context"]
} }
{:ok, activity} = Repo.insert(%Activity{data: activity_data}) {:ok, activity} = Repo.insert(%Activity{data: activity_data, recipients: activity_data["to"]})
user = Repo.get_by(User, ap_id: activity.data["actor"]) user = Repo.get_by(User, ap_id: activity.data["actor"])
{:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user) {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user)

View File

@ -75,17 +75,17 @@ test "an activity" do
date = DateTime.from_naive!(~N[2016-05-24 13:26:08.003], "Etc/UTC") |> DateTime.to_iso8601 date = DateTime.from_naive!(~N[2016-05-24 13:26:08.003], "Etc/UTC") |> DateTime.to_iso8601
{:ok, convo_object} = Object.context_mapping("2hu") |> Repo.insert {:ok, convo_object} = Object.context_mapping("2hu") |> Repo.insert
to = [
User.ap_followers(user),
"https://www.w3.org/ns/activitystreams#Public",
mentioned_user.ap_id
]
activity = %Activity{ activity = %Activity{
id: 1, id: 1,
data: %{ data: %{
"type" => "Create", "type" => "Create",
"id" => "id", "id" => "id",
"to" => [ "to" => to,
User.ap_followers(user),
"https://www.w3.org/ns/activitystreams#Public",
mentioned_user.ap_id
],
"actor" => User.ap_id(user), "actor" => User.ap_id(user),
"object" => %{ "object" => %{
"published" => date, "published" => date,
@ -108,7 +108,8 @@ test "an activity" do
"published" => date, "published" => date,
"context" => "2hu" "context" => "2hu"
}, },
local: false local: false,
recipients: to
} }
expected_html = "<span>2hu</span><br />alert('YAY')Some <img height='32px' width='32px' alt='2hu' title='2hu' src='corndog.png' /> content mentioning <a href=\"#{mentioned_user.ap_id}\">@shp</a>" expected_html = "<span>2hu</span><br />alert('YAY')Some <img height='32px' width='32px' alt='2hu' title='2hu' src='corndog.png' /> content mentioning <a href=\"#{mentioned_user.ap_id}\">@shp</a>"
@ -134,7 +135,7 @@ test "an activity" do
"favorited" => false, "favorited" => false,
"repeated" => false, "repeated" => false,
"external_url" => "some url", "external_url" => "some url",
"tags" => ["content", "mentioning", "nsfw"], "tags" => ["nsfw", "content", "mentioning"],
"activity_type" => "post", "activity_type" => "post",
"possibly_sensitive" => true, "possibly_sensitive" => true,
"uri" => activity.data["object"]["id"] "uri" => activity.data["object"]["id"]

View File

@ -28,4 +28,24 @@ test "represent an image attachment" do
assert expected_object == ObjectRepresenter.to_map(object) assert expected_object == ObjectRepresenter.to_map(object)
end end
test "represents mastodon-style attachments" do
object = %Object{
id: nil,
data: %{
"mediaType" => "image/png",
"name" => "blabla", "type" => "Document",
"url" => "http://mastodon.example.org/system/media_attachments/files/000/000/001/original/8619f31c6edec470.png"
}
}
expected_object = %{
url: "http://mastodon.example.org/system/media_attachments/files/000/000/001/original/8619f31c6edec470.png",
mimetype: "image/png",
oembed: false,
id: nil
}
assert expected_object == ObjectRepresenter.to_map(object)
end
end end

View File

@ -376,9 +376,10 @@ test "without valid credentials", %{conn: conn} do
end end
test "with credentials", %{conn: conn, user: current_user} do test "with credentials", %{conn: conn, user: current_user} do
avatar_image = File.read!("test/fixtures/avatar_data_uri")
conn = conn conn = conn
|> with_credentials(current_user.nickname, "test") |> with_credentials(current_user.nickname, "test")
|> post("/api/qvitter/update_avatar.json", %{img: Pleroma.Web.ActivityPub.ActivityPubTest.data_uri}) |> post("/api/qvitter/update_avatar.json", %{img: avatar_image})
current_user = Repo.get(User, current_user.id) current_user = Repo.get(User, current_user.id)
assert is_map(current_user.avatar) assert is_map(current_user.avatar)

View File

@ -38,9 +38,9 @@ test "create a status" do
assert get_in(activity.data, ["object", "type"]) == "Note" assert get_in(activity.data, ["object", "type"]) == "Note"
assert get_in(activity.data, ["object", "actor"]) == user.ap_id assert get_in(activity.data, ["object", "actor"]) == user.ap_id
assert get_in(activity.data, ["actor"]) == user.ap_id assert get_in(activity.data, ["actor"]) == user.ap_id
assert Enum.member?(get_in(activity.data, ["to"]), User.ap_followers(user)) assert Enum.member?(get_in(activity.data, ["cc"]), User.ap_followers(user))
assert Enum.member?(get_in(activity.data, ["to"]), "https://www.w3.org/ns/activitystreams#Public") assert Enum.member?(get_in(activity.data, ["to"]), "https://www.w3.org/ns/activitystreams#Public")
assert Enum.member?(get_in(activity.data, ["to"]), "shp") assert Enum.member?(get_in(activity.data, ["cc"]), "shp")
assert activity.local == true assert activity.local == true
assert %{"moominmamma" => "http://localhost:4001/finmoji/128px/moominmamma-128.png"} = activity.data["object"]["emoji"] assert %{"moominmamma" => "http://localhost:4001/finmoji/128px/moominmamma-128.png"} = activity.data["object"]["emoji"]
@ -80,7 +80,6 @@ test "create a status that is a reply" do
assert get_in(reply.data, ["object", "context"]) == get_in(activity.data, ["object", "context"]) assert get_in(reply.data, ["object", "context"]) == get_in(activity.data, ["object", "context"])
assert get_in(reply.data, ["object", "inReplyTo"]) == get_in(activity.data, ["object", "id"]) assert get_in(reply.data, ["object", "inReplyTo"]) == get_in(activity.data, ["object", "id"])
assert get_in(reply.data, ["object", "inReplyToStatusId"]) == activity.id assert get_in(reply.data, ["object", "inReplyToStatusId"]) == activity.id
assert Enum.member?(get_in(reply.data, ["to"]), user.ap_id)
end end
test "fetch public statuses, excluding remote ones." do test "fetch public statuses, excluding remote ones." do
@ -99,7 +98,7 @@ test "fetch whole known network statuses" do
%{ public: activity, user: user } = ActivityBuilder.public_and_non_public %{ public: activity, user: user } = ActivityBuilder.public_and_non_public
insert(:note_activity, %{local: false}) insert(:note_activity, %{local: false})
follower = insert(:user, following: [User.ap_followers(user)]) follower = insert(:user, following: [user.follower_address])
statuses = TwitterAPI.fetch_public_and_external_statuses(follower) statuses = TwitterAPI.fetch_public_and_external_statuses(follower)