added support for activitypub follows
This commit is contained in:
parent
0bc0eb23e5
commit
ad450ccf23
@ -4,6 +4,10 @@ namespace App\Http\Controllers\AP;
|
|||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Actor;
|
use App\Models\Actor;
|
||||||
|
use App\Models\Activity;
|
||||||
|
|
||||||
|
use App\Types\TypeActor;
|
||||||
|
use App\Types\TypeActivity;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
@ -13,7 +17,86 @@ class APInboxController extends Controller
|
|||||||
{
|
{
|
||||||
public function inbox (User $user)
|
public function inbox (User $user)
|
||||||
{
|
{
|
||||||
|
$request = request ();
|
||||||
|
$type = $request->get ("type");
|
||||||
|
|
||||||
|
switch ($type) {
|
||||||
|
case "Follow":
|
||||||
|
$this->handle_follow ($user, $request->all ());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "Undo":
|
||||||
|
$this->handle_undo ($user, $request->all ());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
Log::info ("APInboxController@index");
|
Log::info ("APInboxController@index");
|
||||||
Log::info (json_encode (request ()->all ()));
|
Log::info (json_encode ($request->all ()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handle_follow (User $user, $activity)
|
||||||
|
{
|
||||||
|
if (TypeActivity::activity_exists ($activity ["id"]))
|
||||||
|
return response ()->json (["error" => "Activity already exists",], 409);
|
||||||
|
|
||||||
|
$actor = TypeActor::actor_exists_or_obtain ($activity ["actor"]);
|
||||||
|
|
||||||
|
$target = TypeActor::actor_get_local ($activity ["object"]);
|
||||||
|
if (!$target || !$target->user)
|
||||||
|
return response ()->json (["error" => "Target not found",], 404);
|
||||||
|
|
||||||
|
$activity ["activity_id"] = $activity ["id"];
|
||||||
|
|
||||||
|
// there's no follows model, it'll be handled with the activity model
|
||||||
|
$act = Activity::create ($activity);
|
||||||
|
|
||||||
|
// TODO: Users should be able to manually check this
|
||||||
|
$accept_activity = TypeActivity::craft_accept ($act);
|
||||||
|
$response = TypeActivity::post_activity ($accept_activity, $target, $actor);
|
||||||
|
if (!$response)
|
||||||
|
{
|
||||||
|
return response ()->json ([
|
||||||
|
"error" => "Error posting activity",
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
$target->user->friends += 1;
|
||||||
|
$target->user->save ();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle_undo (User $user, $activity)
|
||||||
|
{
|
||||||
|
if (TypeActivity::activity_exists ($activity ["id"]))
|
||||||
|
return response ()->json (["error" => "Activity already exists",], 409);
|
||||||
|
|
||||||
|
$actor = TypeActor::actor_exists_or_obtain ($activity ["actor"]);
|
||||||
|
|
||||||
|
$child_activity = $activity ["object"];
|
||||||
|
if (!TypeActivity::activity_exists ($child_activity ["id"]))
|
||||||
|
return response ()->json (["error" => "Child activity not found",], 404);
|
||||||
|
|
||||||
|
$child_activity = Activity::where ("activity_id", $child_activity ["id"])->first ();
|
||||||
|
switch ($child_activity->type)
|
||||||
|
{
|
||||||
|
case "Follow":
|
||||||
|
// TODO: Move this to its own function
|
||||||
|
// TODO: Should the accept activity be deleted?
|
||||||
|
$followed_user = TypeActor::actor_get_local ($child_activity ["object"]);
|
||||||
|
if (!$followed_user || !$followed_user->user)
|
||||||
|
return response ()->json (["error" => "Target not found",], 404);
|
||||||
|
|
||||||
|
$followed_user->user->friends -= 1;
|
||||||
|
$followed_user->user->save ();
|
||||||
|
|
||||||
|
$child_activity->delete ();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Log::info ("Unknown activity type to Undo: " . $child_activity ["type"]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Should Undo create a new activity model?
|
||||||
|
return response ()->json (["error" => "Not implemented",], 501);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
27
app/Models/Activity.php
Normal file
27
app/Models/Activity.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Activity extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
"activity_id",
|
||||||
|
"actor",
|
||||||
|
"type",
|
||||||
|
"object",
|
||||||
|
"target",
|
||||||
|
"summary"
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
"object" => "array",
|
||||||
|
"target" => "array"
|
||||||
|
];
|
||||||
|
|
||||||
|
public function actor ()
|
||||||
|
{
|
||||||
|
return $this->belongsTo (Actor::class);
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Types\TypeActor;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
@ -28,6 +29,9 @@ class Actor extends Model
|
|||||||
"name",
|
"name",
|
||||||
"summary",
|
"summary",
|
||||||
|
|
||||||
|
"icon",
|
||||||
|
"image",
|
||||||
|
|
||||||
"public_key",
|
"public_key",
|
||||||
"private_key"
|
"private_key"
|
||||||
];
|
];
|
||||||
@ -39,86 +43,12 @@ class Actor extends Model
|
|||||||
|
|
||||||
public function create_from_user (User $user)
|
public function create_from_user (User $user)
|
||||||
{
|
{
|
||||||
$app_url = env ("APP_URL");
|
$data = TypeActor::create_from_user ($user);
|
||||||
|
return $this->create ($data);
|
||||||
$config = [
|
|
||||||
"private_key_bits" => 4096,
|
|
||||||
"private_key_type" => OPENSSL_KEYTYPE_RSA
|
|
||||||
];
|
|
||||||
|
|
||||||
$res = openssl_pkey_new ($config);
|
|
||||||
openssl_pkey_export ($res, $private_key);
|
|
||||||
|
|
||||||
$public_key = openssl_pkey_get_details ($res);
|
|
||||||
|
|
||||||
return $this->create ([
|
|
||||||
"user_id" => $user->id,
|
|
||||||
|
|
||||||
"type" => "Person",
|
|
||||||
"actor_id" => $app_url . "/ap/v1/user/" . $user->name,
|
|
||||||
|
|
||||||
"following" => $app_url . "/ap/v1/user/" . $user->name . "/following",
|
|
||||||
"followers" => $app_url . "/ap/v1/user/" . $user->name . "/followers",
|
|
||||||
|
|
||||||
"liked" => $app_url . "/ap/v1/user/" . $user->name . "/liked",
|
|
||||||
|
|
||||||
"inbox" => $app_url . "/ap/v1/user/" . $user->name . "/inbox",
|
|
||||||
"outbox" => $app_url . "/ap/v1/user/" . $user->name . "/outbox",
|
|
||||||
|
|
||||||
"sharedInbox" => $app_url . "/ap/v1/inbox",
|
|
||||||
|
|
||||||
"preferredUsername" => $user->name,
|
|
||||||
"name" => $user->name,
|
|
||||||
"summary" => "",
|
|
||||||
|
|
||||||
"public_key" => $public_key["key"],
|
|
||||||
"private_key" => $private_key
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function build_response (Actor $actor)
|
public static function build_response (Actor $actor)
|
||||||
{
|
{
|
||||||
$response = [
|
return TypeActor::build_response ($actor);
|
||||||
"@context" => [
|
|
||||||
"https://www.w3.org/ns/activitystreams",
|
|
||||||
"https://w3id.org/security/v1"
|
|
||||||
],
|
|
||||||
"id" => $actor->actor_id,
|
|
||||||
"type" => $actor->type,
|
|
||||||
|
|
||||||
"following" => $actor->following,
|
|
||||||
"followers" => $actor->followers,
|
|
||||||
|
|
||||||
"liked" => $actor->liked,
|
|
||||||
|
|
||||||
"inbox" => $actor->inbox,
|
|
||||||
"outbox" => $actor->outbox,
|
|
||||||
|
|
||||||
"sharedInbox" => $actor->sharedInbox,
|
|
||||||
|
|
||||||
"preferredUsername" => $actor->preferredUsername,
|
|
||||||
"name" => $actor->name,
|
|
||||||
"summary" => $actor->summary,
|
|
||||||
|
|
||||||
"icon" => [
|
|
||||||
"type" => "Image",
|
|
||||||
"mediaType" => "image/jpeg",
|
|
||||||
"url" => $actor->icon
|
|
||||||
],
|
|
||||||
|
|
||||||
"image" => [
|
|
||||||
"type" => "Image",
|
|
||||||
"mediaType" => "image/jpeg",
|
|
||||||
"url" => $actor->icon
|
|
||||||
],
|
|
||||||
|
|
||||||
"publicKey" => [
|
|
||||||
"id" => $actor->actor_id . "#main-key",
|
|
||||||
"owner" => $actor->actor_id,
|
|
||||||
"publicKeyPem" => $actor->public_key
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
114
app/Types/TypeActivity.php
Normal file
114
app/Types/TypeActivity.php
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Types;
|
||||||
|
|
||||||
|
use App\Models\Actor;
|
||||||
|
use App\Models\Activity;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class TypeActivity {
|
||||||
|
public static function craft_response (Activity $activity)
|
||||||
|
{
|
||||||
|
$crafted_activity = [
|
||||||
|
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id" => $activity->activity_id,
|
||||||
|
"type" => $activity->type,
|
||||||
|
"actor" => $activity->actor,
|
||||||
|
"object" => $activity->object,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($activity->target)
|
||||||
|
{
|
||||||
|
$crafted_activity["target"] = $activity->target;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($activity->summary)
|
||||||
|
{
|
||||||
|
$crafted_activity["summary"] = $activity->summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $crafted_activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function craft_accept (Activity $activity)
|
||||||
|
{
|
||||||
|
$accept_activity = new Activity ();
|
||||||
|
$accept_activity->activity_id = env ("APP_URL") . "/activity/" . uniqid ();
|
||||||
|
$accept_activity->type = "Accept";
|
||||||
|
$accept_activity->actor = $activity->object;
|
||||||
|
$accept_activity->object = TypeActivity::craft_response ($activity);
|
||||||
|
$accept_activity->save ();
|
||||||
|
|
||||||
|
return $accept_activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function craft_signed_headers ($activity, Actor $source, Actor $target)
|
||||||
|
{
|
||||||
|
if (!$source->user)
|
||||||
|
{
|
||||||
|
Log::error ("Source not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key_id = $source->actor_id . "#main-key";
|
||||||
|
|
||||||
|
$signer = openssl_get_privatekey ($source->private_key);
|
||||||
|
|
||||||
|
$date = gmdate ("D, d M Y H:i:s \G\M\T");
|
||||||
|
|
||||||
|
// we suppose that the activity is already json encoded
|
||||||
|
$hash = hash ("sha256", $activity, true);
|
||||||
|
$digest = base64_encode ($hash);
|
||||||
|
|
||||||
|
$url = parse_url ($target->inbox);
|
||||||
|
$string_to_sign = "(request-target): post ". $url["path"] . "\nhost: " . $url["host"] . "\ndate: " . $date . "\ndigest: SHA-256=" . $digest;
|
||||||
|
|
||||||
|
openssl_sign ($string_to_sign, $signature, $signer, OPENSSL_ALGO_SHA256);
|
||||||
|
$signature_b64 = base64_encode ($signature);
|
||||||
|
|
||||||
|
$signature_header = 'keyId="' . $key_id . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
|
||||||
|
|
||||||
|
return [
|
||||||
|
"Host" => $url["host"],
|
||||||
|
"Date" => $date,
|
||||||
|
"Digest" => "SHA-256=" . $digest,
|
||||||
|
"Signature" => $signature_header,
|
||||||
|
"Content-Type" => "application/activity+json",
|
||||||
|
"Accept" => "application/activity+json",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function post_activity (Activity $activity, Actor $source, Actor $target)
|
||||||
|
{
|
||||||
|
$crafted_activity = TypeActivity::craft_response ($activity);
|
||||||
|
$activity_json = json_encode ($crafted_activity, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION);
|
||||||
|
$activity_json = mb_convert_encoding ($activity_json, "UTF-8");
|
||||||
|
|
||||||
|
$headers = TypeActivity::craft_signed_headers ($activity_json, $source, $target);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$client = new Client ();
|
||||||
|
$response = $client->post ($target->inbox, [
|
||||||
|
"headers" => $headers,
|
||||||
|
"body" => $activity_json,
|
||||||
|
"debug" => true
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
catch (RequestException $e)
|
||||||
|
{
|
||||||
|
Log::error ($e->getMessage ());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// some little functions
|
||||||
|
public static function activity_exists ($activity_id)
|
||||||
|
{
|
||||||
|
return Activity::where ("activity_id", $activity_id)->first ();
|
||||||
|
}
|
||||||
|
}
|
200
app/Types/TypeActor.php
Normal file
200
app/Types/TypeActor.php
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Types;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Actor;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class TypeActor {
|
||||||
|
public static function gen_keys ()
|
||||||
|
{
|
||||||
|
$config = [
|
||||||
|
"private_key_bits" => 4096,
|
||||||
|
"private_key_type" => OPENSSL_KEYTYPE_RSA
|
||||||
|
];
|
||||||
|
|
||||||
|
$res = openssl_pkey_new ($config);
|
||||||
|
openssl_pkey_export ($res, $private_key);
|
||||||
|
|
||||||
|
$public_key = openssl_pkey_get_details ($res);
|
||||||
|
|
||||||
|
return [
|
||||||
|
"public_key" => $public_key,
|
||||||
|
"private_key" => $private_key
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function create_from_user (User $user)
|
||||||
|
{
|
||||||
|
$keys = TypeActor::gen_keys ();
|
||||||
|
$app_url = env ("APP_URL");
|
||||||
|
|
||||||
|
return [
|
||||||
|
"user_id" => $user->id,
|
||||||
|
|
||||||
|
"type" => "Person",
|
||||||
|
"actor_id" => $app_url . "/ap/v1/user/" . $user->name,
|
||||||
|
|
||||||
|
"following" => $app_url . "/ap/v1/user/" . $user->name . "/following",
|
||||||
|
"followers" => $app_url . "/ap/v1/user/" . $user->name . "/followers",
|
||||||
|
|
||||||
|
"liked" => $app_url . "/ap/v1/user/" . $user->name . "/liked",
|
||||||
|
|
||||||
|
"inbox" => $app_url . "/ap/v1/user/" . $user->name . "/inbox",
|
||||||
|
"outbox" => $app_url . "/ap/v1/user/" . $user->name . "/outbox",
|
||||||
|
|
||||||
|
"sharedInbox" => $app_url . "/ap/v1/inbox",
|
||||||
|
|
||||||
|
"preferredUsername" => $user->name,
|
||||||
|
"name" => $user->name,
|
||||||
|
"summary" => "",
|
||||||
|
|
||||||
|
"public_key" => $keys["public_key"]["key"],
|
||||||
|
"private_key" => $keys["private_key"]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function build_response (Actor $actor)
|
||||||
|
{
|
||||||
|
$response = [
|
||||||
|
"@context" => [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1"
|
||||||
|
],
|
||||||
|
"id" => $actor->actor_id,
|
||||||
|
"type" => $actor->type,
|
||||||
|
|
||||||
|
"following" => $actor->following,
|
||||||
|
"followers" => $actor->followers,
|
||||||
|
|
||||||
|
"liked" => $actor->liked,
|
||||||
|
|
||||||
|
"inbox" => $actor->inbox,
|
||||||
|
"outbox" => $actor->outbox,
|
||||||
|
|
||||||
|
"endpoints" => [
|
||||||
|
"sharedInbox" => $actor->sharedInbox,
|
||||||
|
],
|
||||||
|
|
||||||
|
"preferredUsername" => $actor->preferredUsername,
|
||||||
|
"name" => $actor->name,
|
||||||
|
"summary" => $actor->summary,
|
||||||
|
|
||||||
|
"icon" => [
|
||||||
|
"type" => "Image",
|
||||||
|
"mediaType" => "image/png",
|
||||||
|
"url" => $actor->icon
|
||||||
|
],
|
||||||
|
|
||||||
|
"image" => [
|
||||||
|
"type" => "Image",
|
||||||
|
"mediaType" => "image/png",
|
||||||
|
"url" => $actor->image
|
||||||
|
],
|
||||||
|
|
||||||
|
"publicKey" => [
|
||||||
|
"id" => $actor->actor_id . "#main-key",
|
||||||
|
"owner" => $actor->actor_id,
|
||||||
|
"publicKeyPem" => $actor->public_key
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function create_from_request ($request)
|
||||||
|
{
|
||||||
|
$actor = new Actor ();
|
||||||
|
|
||||||
|
// Use null coalescing operator `??` for safety
|
||||||
|
$actor->actor_id = $request['id'] ?? '';
|
||||||
|
$actor->type = $request['type'] ?? '';
|
||||||
|
|
||||||
|
$actor->following = $request['following'] ?? '';
|
||||||
|
$actor->followers = $request['followers'] ?? '';
|
||||||
|
|
||||||
|
$actor->liked = $request['liked'] ?? '';
|
||||||
|
|
||||||
|
$actor->inbox = $request['inbox'] ?? '';
|
||||||
|
$actor->outbox = $request['outbox'] ?? '';
|
||||||
|
|
||||||
|
$actor->sharedInbox = $request['endpoints']['sharedInbox'] ?? '';
|
||||||
|
|
||||||
|
$actor->preferredUsername = $request['preferredUsername'] ?? '';
|
||||||
|
$actor->name = $request['name'] ?? '';
|
||||||
|
$actor->summary = $request['summary'] ?? '';
|
||||||
|
|
||||||
|
// Handle nested keys with checks
|
||||||
|
$actor->icon = $request['icon']['url'] ?? '';
|
||||||
|
$actor->image = $request['image']['url'] ?? '';
|
||||||
|
|
||||||
|
// Handle nested keys in `publicKey`
|
||||||
|
$actor->public_key = $request['publicKey']['publicKeyPem'] ?? '';
|
||||||
|
|
||||||
|
$actor->save ();
|
||||||
|
|
||||||
|
return $actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function obtain_actor_info ($actor_id)
|
||||||
|
{
|
||||||
|
$client = new Client ();
|
||||||
|
|
||||||
|
$parsed_url = parse_url ($actor_id);
|
||||||
|
$url_instance = $parsed_url["scheme"] . "://" . $parsed_url["host"];
|
||||||
|
$url_path = explode ("/", $parsed_url["path"]);
|
||||||
|
$actor_name = end ($url_path);
|
||||||
|
|
||||||
|
$well_known_url = $url_instance . "/.well-known/webfinger?resource=acct:" . $actor_name . "@" . $parsed_url["host"];
|
||||||
|
$res = $client->get ($well_known_url);
|
||||||
|
|
||||||
|
$response = json_decode ($res->getBody ()->getContents ());
|
||||||
|
|
||||||
|
foreach ($response->links as $link)
|
||||||
|
{
|
||||||
|
if ($link->rel == "self")
|
||||||
|
{
|
||||||
|
$res = $client->request ("GET", $link->href, [
|
||||||
|
"headers" => [
|
||||||
|
"Accept" => "application/activity+json"
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
$actor = json_decode ($res->getBody ()->getContents (), true);
|
||||||
|
|
||||||
|
$result = TypeActor::create_from_request ($actor);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// some little functions
|
||||||
|
public static function actor_exists ($actor_id)
|
||||||
|
{
|
||||||
|
$actor = Actor::where ("actor_id", $actor_id)->first ();
|
||||||
|
return $actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function actor_exists_or_obtain ($actor_id)
|
||||||
|
{
|
||||||
|
$actor = TypeActor::actor_exists ($actor_id);
|
||||||
|
if (!$actor)
|
||||||
|
{
|
||||||
|
$actor = TypeActor::obtain_actor_info ($actor_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function actor_get_local ($actor_id)
|
||||||
|
{
|
||||||
|
$actor = Actor::where ("actor_id", $actor_id)->first ();
|
||||||
|
if (!$actor->user)
|
||||||
|
return null;
|
||||||
|
return $actor;
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
|
"guzzlehttp/guzzle": "^7.9",
|
||||||
"intervention/image": "^3.10",
|
"intervention/image": "^3.10",
|
||||||
"laravel/framework": "^11.31",
|
"laravel/framework": "^11.31",
|
||||||
"laravel/tinker": "^2.9"
|
"laravel/tinker": "^2.9"
|
||||||
|
2
composer.lock
generated
2
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "64b78dd438697d218ce81a8ea6afcbb3",
|
"content-hash": "067a260457c754c554a61fce0e741b0f",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
|
@ -37,6 +37,7 @@ return new class extends Migration
|
|||||||
$table->string ("private_key")->nullable ();
|
$table->string ("private_key")->nullable ();
|
||||||
|
|
||||||
$table->string ("icon")->nullable ();
|
$table->string ("icon")->nullable ();
|
||||||
|
$table->string ("image")->nullable ();
|
||||||
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('activities', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->string ("activity_id")->unique ();
|
||||||
|
$table->string ("actor");
|
||||||
|
$table->string ("type");
|
||||||
|
$table->json ("object");
|
||||||
|
$table->json ("target")->nullable ();
|
||||||
|
$table->text ("summary")->nullable ();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('activities');
|
||||||
|
}
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user