Update
This commit is contained in:
parent
aac9bcb6e4
commit
3f329cf59e
13 changed files with 191 additions and 119 deletions
|
@ -16,10 +16,14 @@ config :nulla, Nulla.Repo,
|
||||||
# The watchers configuration can be used to run external
|
# The watchers configuration can be used to run external
|
||||||
# watchers to your application. For example, we can use it
|
# watchers to your application. For example, we can use it
|
||||||
# to bundle .js and .css sources.
|
# to bundle .js and .css sources.
|
||||||
|
host = System.get_env("PHX_HOST") || "example.com"
|
||||||
|
port = String.to_integer(System.get_env("PORT") || "4000")
|
||||||
|
|
||||||
config :nulla, NullaWeb.Endpoint,
|
config :nulla, NullaWeb.Endpoint,
|
||||||
# Binding to loopback ipv4 address prevents access from other machines.
|
# Binding to loopback ipv4 address prevents access from other machines.
|
||||||
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
|
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
|
||||||
http: [ip: {127, 0, 0, 1}, port: 4000],
|
url: [host: host, port: port],
|
||||||
|
http: [ip: {127, 0, 0, 1}, port: port],
|
||||||
check_origin: false,
|
check_origin: false,
|
||||||
code_reloader: true,
|
code_reloader: true,
|
||||||
debug_errors: true,
|
debug_errors: true,
|
||||||
|
|
|
@ -51,7 +51,12 @@ defmodule Nulla.ActivityPub do
|
||||||
indexable: actor.indexable,
|
indexable: actor.indexable,
|
||||||
published: DateTime.to_iso8601(actor.published),
|
published: DateTime.to_iso8601(actor.published),
|
||||||
memorial: actor.memorial,
|
memorial: actor.memorial,
|
||||||
publicKey: actor.publicKey,
|
publicKey:
|
||||||
|
Jason.OrderedObject.new(
|
||||||
|
id: actor.publicKey["id"],
|
||||||
|
owner: actor.publicKey["owner"],
|
||||||
|
publicKeyPem: actor.publicKey["publicKeyPem"]
|
||||||
|
),
|
||||||
tag: actor.tag,
|
tag: actor.tag,
|
||||||
attachment: actor.attachment,
|
attachment: actor.attachment,
|
||||||
endpoints: actor.endpoints,
|
endpoints: actor.endpoints,
|
||||||
|
|
|
@ -1,18 +1,23 @@
|
||||||
defmodule Nulla.KeyGen do
|
defmodule Nulla.KeyGen do
|
||||||
def gen do
|
def gen do
|
||||||
rsa_key = :public_key.generate_key({:rsa, 2048, 65537})
|
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||||
|
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
||||||
|
|
||||||
{:RSAPrivateKey, :"two-prime", n, e, _d, _p, _q, _dp, _dq, _qi, _other} = rsa_key
|
private_pem =
|
||||||
public_key = {:RSAPublicKey, n, e}
|
:public_key.pem_encode([entry])
|
||||||
|
|> String.trim_trailing()
|
||||||
|
|> Kernel.<>("\n")
|
||||||
|
|
||||||
private_entry =
|
[private_key_code] = :public_key.pem_decode(private_pem)
|
||||||
{:PrivateKeyInfo, :public_key.der_encode(:RSAPrivateKey, rsa_key), :not_encrypted}
|
private_key = :public_key.pem_entry_decode(private_key_code)
|
||||||
|
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key
|
||||||
|
public_key = {:RSAPublicKey, modulus, exponent}
|
||||||
|
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||||
|
|
||||||
public_entry =
|
public_pem =
|
||||||
{:SubjectPublicKeyInfo, :public_key.der_encode(:RSAPublicKey, public_key), :not_encrypted}
|
:public_key.pem_encode([public_key])
|
||||||
|
|> String.trim_trailing()
|
||||||
private_pem = :public_key.pem_encode([private_entry])
|
|> Kernel.<>("\n")
|
||||||
public_pem = :public_key.pem_encode([public_entry])
|
|
||||||
|
|
||||||
{public_pem, private_pem}
|
{public_pem, private_pem}
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,13 +4,13 @@ defmodule Nulla.Models.Activity do
|
||||||
alias Nulla.Repo
|
alias Nulla.Repo
|
||||||
alias Nulla.Snowflake
|
alias Nulla.Snowflake
|
||||||
|
|
||||||
|
@derive {Jason.Encoder, only: [:ap_id, :type, :actor, :object]}
|
||||||
@primary_key {:id, :integer, autogenerate: false}
|
@primary_key {:id, :integer, autogenerate: false}
|
||||||
schema "activities" do
|
schema "activities" do
|
||||||
field :ap_id, :string
|
field :ap_id, :string
|
||||||
field :type, :string
|
field :type, :string
|
||||||
field :actor, :string
|
field :actor, :string
|
||||||
field :object, :map
|
field :object, :string
|
||||||
field :cc, {:array, :string}, default: []
|
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
@ -18,7 +18,7 @@ defmodule Nulla.Models.Activity do
|
||||||
@doc false
|
@doc false
|
||||||
def changeset(activity, attrs) do
|
def changeset(activity, attrs) do
|
||||||
activity
|
activity
|
||||||
|> cast(attrs, [:ap_id, :type, :actor, :object, :to])
|
|> cast(attrs, [:ap_id, :type, :actor, :object])
|
||||||
|> validate_required([:ap_id, :type, :actor, :object])
|
|> validate_required([:ap_id, :type, :actor, :object])
|
||||||
|> validate_inclusion(:type, ~w(Create Update Delete Undo Like Announce Follow Accept Reject))
|
|> validate_inclusion(:type, ~w(Create Update Delete Undo Like Announce Follow Accept Reject))
|
||||||
end
|
end
|
||||||
|
|
|
@ -78,15 +78,8 @@ defmodule Nulla.Models.Actor do
|
||||||
:followers,
|
:followers,
|
||||||
:inbox,
|
:inbox,
|
||||||
:outbox,
|
:outbox,
|
||||||
:featured,
|
|
||||||
:featuredTags,
|
|
||||||
:preferredUsername,
|
:preferredUsername,
|
||||||
:url,
|
:url,
|
||||||
:manuallyApprovesFollowers,
|
|
||||||
:discoverable,
|
|
||||||
:indexable,
|
|
||||||
:published,
|
|
||||||
:memorial,
|
|
||||||
:publicKey,
|
:publicKey,
|
||||||
:endpoints
|
:endpoints
|
||||||
])
|
])
|
||||||
|
|
|
@ -48,7 +48,7 @@ defmodule Nulla.Models.Relation do
|
||||||
:local_actor_id,
|
:local_actor_id,
|
||||||
:remote_actor_id
|
:remote_actor_id
|
||||||
])
|
])
|
||||||
|> validate_required([:id, :local_actor_id, :remote_actor_id])
|
|> validate_required([:local_actor_id, :remote_actor_id])
|
||||||
|> unique_constraint([:local_actor_id, :remote_actor_id])
|
|> unique_constraint([:local_actor_id, :remote_actor_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -61,6 +61,10 @@ defmodule Nulla.Models.Relation do
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_relation(by) when is_map(by) or is_list(by) do
|
||||||
|
Repo.get_by(__MODULE__, by)
|
||||||
|
end
|
||||||
|
|
||||||
def count_following(local_actor_id) do
|
def count_following(local_actor_id) do
|
||||||
__MODULE__
|
__MODULE__
|
||||||
|> where([r], r.local_actor_id == ^local_actor_id and r.following == true)
|
|> where([r], r.local_actor_id == ^local_actor_id and r.following == true)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
defmodule Nulla.Utils do
|
defmodule Nulla.Utils do
|
||||||
|
alias Finch
|
||||||
|
|
||||||
def fetch_remote_actor(uri) do
|
def fetch_remote_actor(uri) do
|
||||||
headers = [
|
headers = [
|
||||||
{"Accept", "application/activity+json"},
|
{"Accept", "application/activity+json"},
|
||||||
|
@ -8,7 +10,7 @@ defmodule Nulla.Utils do
|
||||||
|
|
||||||
request = Finch.build(:get, uri, headers)
|
request = Finch.build(:get, uri, headers)
|
||||||
|
|
||||||
case Finch.request(request, Finch) do
|
case Finch.request(request, Nulla.Finch) do
|
||||||
{:ok, %Finch.Response{status: 200, body: body}} ->
|
{:ok, %Finch.Response{status: 200, body: body}} ->
|
||||||
case Jason.decode(body) do
|
case Jason.decode(body) do
|
||||||
{:ok, data} -> {:ok, data}
|
{:ok, data} -> {:ok, data}
|
||||||
|
|
|
@ -17,7 +17,7 @@ defmodule NullaWeb.ActorController do
|
||||||
|> json(%{error: "Not Found"})
|
|> json(%{error: "Not Found"})
|
||||||
|
|
||||||
%Actor{} = actor ->
|
%Actor{} = actor ->
|
||||||
if accept in ["application/activity+json", "application/ld+json"] do
|
if accept =~ "json" do
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("application/activity+json")
|
|> put_resp_content_type("application/activity+json")
|
||||||
|> send_resp(200, Jason.encode!(ActivityPub.actor(actor)))
|
|> send_resp(200, Jason.encode!(ActivityPub.actor(actor)))
|
||||||
|
|
|
@ -18,7 +18,7 @@ defmodule NullaWeb.FollowController do
|
||||||
_ -> 1
|
_ -> 1
|
||||||
end
|
end
|
||||||
|
|
||||||
following_list = Relation.get_following(actor.id, page, limit)
|
following_list = Enum.map(Relation.get_following(actor.id, page, limit), & &1.ap_id)
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("application/activity+json")
|
|> put_resp_content_type("application/activity+json")
|
||||||
|
@ -49,7 +49,7 @@ defmodule NullaWeb.FollowController do
|
||||||
_ -> 1
|
_ -> 1
|
||||||
end
|
end
|
||||||
|
|
||||||
followers_list = Relation.get_followers(actor.id, page, limit)
|
followers_list = Enum.map(Relation.get_followers(actor.id, page, limit), & &1.ap_id)
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("application/activity+json")
|
|> put_resp_content_type("application/activity+json")
|
||||||
|
|
|
@ -1,106 +1,165 @@
|
||||||
defmodule NullaWeb.InboxController do
|
defmodule NullaWeb.InboxController do
|
||||||
use NullaWeb, :controller
|
use NullaWeb, :controller
|
||||||
alias Nulla.HTTPSignature
|
|
||||||
alias Nulla.ActivityPub
|
alias Nulla.ActivityPub
|
||||||
alias Nulla.Snowflake
|
alias Nulla.Snowflake
|
||||||
|
alias Nulla.HTTPSignature
|
||||||
alias Nulla.Utils
|
alias Nulla.Utils
|
||||||
alias Nulla.Models.User
|
alias Nulla.Models.User
|
||||||
alias Nulla.Models.Actor
|
alias Nulla.Models.Actor
|
||||||
alias Nulla.Models.Relation
|
alias Nulla.Models.Relation
|
||||||
alias Nulla.Models.Activity
|
alias Nulla.Models.Activity
|
||||||
|
|
||||||
def inbox(
|
def inbox(conn, %{
|
||||||
conn,
|
"id" => follow_id,
|
||||||
%{"id" => follow_id, "type" => "Follow", "actor" => actor_uri, "object" => target_uri}
|
"type" => "Follow",
|
||||||
) do
|
"actor" => actor_uri,
|
||||||
|
"object" => target_uri
|
||||||
|
}) do
|
||||||
accept_id = Snowflake.next_id()
|
accept_id = Snowflake.next_id()
|
||||||
|
|
||||||
with local_actor <- Actor.get_actor(ap_id: target_uri),
|
with local_actor <- Actor.get_actor(ap_id: target_uri),
|
||||||
{:ok, remote_actor_json} <- Utils.fetch_remote_actor(actor_uri),
|
{:ok, remote_actor_json} <- Utils.fetch_remote_actor(actor_uri),
|
||||||
:ok <- HTTPSignature.verify(conn, remote_actor_json),
|
:ok <- HTTPSignature.verify(conn, remote_actor_json),
|
||||||
remote_actor <-
|
{:ok, remote_actor} <- get_or_create_actor(remote_actor_json),
|
||||||
Actor.create_actor(
|
{:ok, follow_activity} <-
|
||||||
remote_actor_json
|
create_follow_activity(follow_id, remote_actor.ap_id, target_uri),
|
||||||
|> Map.put("ap_id", remote_actor_json["id"])
|
{:ok, accept_activity} <-
|
||||||
|> Map.delete("id")
|
create_accept_activity(accept_id, local_actor, follow_activity),
|
||||||
|> Map.put("domain", URI.parse(remote_actor_json["id"]).host)
|
{:ok, _relation} <- get_or_create_relation(local_actor.id, remote_actor.id),
|
||||||
),
|
:ok <- deliver_accept(accept_activity, remote_actor_json, local_actor) do
|
||||||
follow_activity <-
|
send_resp(conn, 200, "")
|
||||||
Activity.create_activity(%{
|
|
||||||
ap_id: follow_id,
|
|
||||||
type: "Follow",
|
|
||||||
actor: remote_actor.id,
|
|
||||||
object: target_uri
|
|
||||||
}),
|
|
||||||
accept_activity <-
|
|
||||||
Activity.create_activity(%{
|
|
||||||
id: accept_id,
|
|
||||||
ap_id: "https://#{local_actor.domain}/activities/accept/#{accept_id}",
|
|
||||||
type: "Accept",
|
|
||||||
actor: local_actor.ap_id,
|
|
||||||
object: follow_activity
|
|
||||||
}),
|
|
||||||
_ <-
|
|
||||||
Relation.create_relation(%{
|
|
||||||
followed_by: true,
|
|
||||||
local_actor_id: local_actor.id,
|
|
||||||
remote_actor_id: remote_actor.id
|
|
||||||
}) do
|
|
||||||
body = Jason.encode!(ActivityPub.activity(accept_activity))
|
|
||||||
|
|
||||||
digest =
|
|
||||||
:crypto.hash(:sha256, body)
|
|
||||||
|> Base.encode64()
|
|
||||||
|> then(&("SHA-256=" <> &1))
|
|
||||||
|
|
||||||
date =
|
|
||||||
DateTime.utc_now()
|
|
||||||
|> Calendar.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
|
||||||
|
|
||||||
host = URI.parse(actor_uri).host
|
|
||||||
request_target = "post /inbox"
|
|
||||||
|
|
||||||
signature_string =
|
|
||||||
"""
|
|
||||||
(request-target): #{request_target}
|
|
||||||
host: #{host}
|
|
||||||
date: #{date}
|
|
||||||
digest: #{digest}
|
|
||||||
"""
|
|
||||||
|
|
||||||
user = User.get_user(id: local_actor.id)
|
|
||||||
privateKeyPem = user.privateKey["privateKeyPem"]
|
|
||||||
|
|
||||||
private_key =
|
|
||||||
:public_key.pem_decode(privateKeyPem)
|
|
||||||
|> hd()
|
|
||||||
|> :public_key.pem_entry_decode()
|
|
||||||
|
|
||||||
signature =
|
|
||||||
:public_key.sign(signature_string, :sha256, private_key)
|
|
||||||
|> Base.encode64()
|
|
||||||
|
|
||||||
signature_header =
|
|
||||||
"""
|
|
||||||
keyId="#{local_actor.publicKey["id"]}",
|
|
||||||
algorithm="rsa-sha256",
|
|
||||||
headers="(request-target) host date digest content-type",
|
|
||||||
signature="#{signature}"
|
|
||||||
"""
|
|
||||||
|> String.replace("\n", "")
|
|
||||||
|> String.trim()
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> put_resp_content_type("application/activity+json")
|
|
||||||
|> put_resp_header("host", host)
|
|
||||||
|> put_resp_header("date", date)
|
|
||||||
|> put_resp_header("signature", signature_header)
|
|
||||||
|> put_resp_header("digest", digest)
|
|
||||||
|> send_resp(200, body)
|
|
||||||
else
|
else
|
||||||
error ->
|
error ->
|
||||||
IO.inspect(error, label: "Follow error")
|
IO.inspect(error, label: "Follow error")
|
||||||
json(conn, %{"error" => "Failed to process Follow"})
|
json(conn, %{"error" => "Failed to process Follow"})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp get_or_create_actor(remote_actor_json) do
|
||||||
|
ap_id = remote_actor_json["id"]
|
||||||
|
|
||||||
|
case Actor.get_actor(ap_id: ap_id) do
|
||||||
|
nil ->
|
||||||
|
params =
|
||||||
|
remote_actor_json
|
||||||
|
|> Map.put("ap_id", ap_id)
|
||||||
|
|> Map.delete("id")
|
||||||
|
|> Map.put("domain", URI.parse(ap_id).host)
|
||||||
|
|
||||||
|
case Actor.create_actor(params) do
|
||||||
|
{:ok, actor} -> {:ok, actor}
|
||||||
|
{:error, changeset} -> {:error, {:actor_creation_failed, changeset}}
|
||||||
|
end
|
||||||
|
|
||||||
|
actor ->
|
||||||
|
{:ok, actor}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_follow_activity(follow_id, actor_id, target_uri) do
|
||||||
|
Activity.create_activity(%{
|
||||||
|
ap_id: follow_id,
|
||||||
|
type: "Follow",
|
||||||
|
actor: actor_id,
|
||||||
|
object: target_uri
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_accept_activity(accept_id, local_actor, follow_activity) do
|
||||||
|
Activity.create_activity(%{
|
||||||
|
id: accept_id,
|
||||||
|
ap_id: "https://#{local_actor.domain}/activities/accept/#{accept_id}",
|
||||||
|
type: "Accept",
|
||||||
|
actor: local_actor.ap_id,
|
||||||
|
object: Jason.encode!(follow_activity)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_or_create_relation(local_actor_id, remote_actor_id) do
|
||||||
|
case Relation.get_relation(local_actor_id: local_actor_id, remote_actor_id: remote_actor_id) do
|
||||||
|
nil ->
|
||||||
|
Relation.create_relation(%{
|
||||||
|
followed_by: true,
|
||||||
|
local_actor_id: local_actor_id,
|
||||||
|
remote_actor_id: remote_actor_id
|
||||||
|
})
|
||||||
|
|
||||||
|
relation ->
|
||||||
|
{:ok, relation}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp deliver_accept(accept_activity, remote_actor_json, local_actor) do
|
||||||
|
inbox_url = remote_actor_json["inbox"]
|
||||||
|
accept_activity = %Activity{accept_activity | object: Jason.decode!(accept_activity.object)}
|
||||||
|
body = Jason.encode!(ActivityPub.activity(accept_activity))
|
||||||
|
|
||||||
|
digest =
|
||||||
|
:crypto.hash(:sha256, body)
|
||||||
|
|> Base.encode64()
|
||||||
|
|> then(&("SHA-256=" <> &1))
|
||||||
|
|
||||||
|
date =
|
||||||
|
DateTime.utc_now()
|
||||||
|
|> Calendar.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
||||||
|
|
||||||
|
uri = URI.parse(inbox_url)
|
||||||
|
request_target = "post #{uri.path}"
|
||||||
|
|
||||||
|
signature_string = """
|
||||||
|
(request-target): #{request_target}
|
||||||
|
host: #{uri.host}
|
||||||
|
date: #{date}
|
||||||
|
digest: #{digest}
|
||||||
|
"""
|
||||||
|
|
||||||
|
user = User.get_user(id: local_actor.id)
|
||||||
|
|
||||||
|
private_key =
|
||||||
|
case :public_key.pem_decode(user.privateKeyPem) do
|
||||||
|
[{_type, der, _rest}] ->
|
||||||
|
:public_key.der_decode(:RSAPrivateKey, der)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
raise "Invalid PEM format"
|
||||||
|
end
|
||||||
|
|
||||||
|
signature =
|
||||||
|
:public_key.sign(signature_string, :sha256, private_key)
|
||||||
|
|> Base.encode64()
|
||||||
|
|
||||||
|
signature_header =
|
||||||
|
"""
|
||||||
|
keyId="#{local_actor.publicKey["id"]}",
|
||||||
|
algorithm="rsa-sha256",
|
||||||
|
headers="(request-target) host date digest content-type",
|
||||||
|
signature="#{signature}"
|
||||||
|
"""
|
||||||
|
|> String.replace("\n", "")
|
||||||
|
|> String.trim()
|
||||||
|
|
||||||
|
headers = [
|
||||||
|
{"host", uri.host},
|
||||||
|
{"date", date},
|
||||||
|
{"digest", digest},
|
||||||
|
{"signature", signature_header},
|
||||||
|
{"content-type", "application/activity+json"}
|
||||||
|
]
|
||||||
|
|
||||||
|
request = Finch.build(:post, inbox_url, headers, body)
|
||||||
|
|
||||||
|
case Finch.request(request, Nulla.Finch) do
|
||||||
|
{:ok, %Finch.Response{status: code}} when code in 200..299 ->
|
||||||
|
IO.puts("Accept delivered successfully")
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Finch.Response{status: code, body: resp}} ->
|
||||||
|
IO.inspect({:error, code, resp}, label: "Failed to deliver Accept")
|
||||||
|
{:error, {:http_error, code}}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
IO.inspect(reason, label: "Finch delivery failed")
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,12 +2,12 @@ defmodule NullaWeb.Router do
|
||||||
use NullaWeb, :router
|
use NullaWeb, :router
|
||||||
|
|
||||||
pipeline :browser do
|
pipeline :browser do
|
||||||
plug :accepts, ["html", "activity+json", "ld+json"]
|
plug :accepts, ["html", "json", "activity+json", "ld+json"]
|
||||||
plug :fetch_session
|
plug :fetch_session
|
||||||
plug :fetch_live_flash
|
plug :fetch_live_flash
|
||||||
plug :put_root_layout, html: {NullaWeb.Layouts, :root}
|
plug :put_root_layout, html: {NullaWeb.Layouts, :root}
|
||||||
plug :protect_from_forgery
|
# plug :protect_from_forgery
|
||||||
plug :put_secure_browser_headers
|
# plug :put_secure_browser_headers
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :api do
|
pipeline :api do
|
||||||
|
|
|
@ -15,7 +15,7 @@ defmodule Nulla.Repo.Migrations.CreateActors do
|
||||||
add :featuredTags, :string
|
add :featuredTags, :string
|
||||||
add :preferredUsername, :string, null: false
|
add :preferredUsername, :string, null: false
|
||||||
add :name, :string
|
add :name, :string
|
||||||
add :summary, :string
|
add :summary, :text
|
||||||
add :url, :string
|
add :url, :string
|
||||||
add :manuallyApprovesFollowers, :boolean
|
add :manuallyApprovesFollowers, :boolean
|
||||||
add :discoverable, :boolean, default: true
|
add :discoverable, :boolean, default: true
|
||||||
|
|
|
@ -6,14 +6,14 @@ defmodule Nulla.Repo.Migrations.CreateActivities do
|
||||||
add :id, :bigint, primary_key: true
|
add :id, :bigint, primary_key: true
|
||||||
add :ap_id, :string, null: false
|
add :ap_id, :string, null: false
|
||||||
add :type, :string, null: false
|
add :type, :string, null: false
|
||||||
add :actor_id, references(:actors, type: :bigint, on_delete: :nothing), null: false
|
add :actor, :string, null: false
|
||||||
add :object, :map, null: false
|
add :object, :text, null: false
|
||||||
add :to, {:array, :string}, default: []
|
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create index(:activities, [:ap_id])
|
||||||
create index(:activities, [:type])
|
create index(:activities, [:type])
|
||||||
create index(:activities, [:actor_id])
|
create index(:activities, [:actor])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue