Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
f9f2744fad |
|||
|
31c49ac3fc |
|||
|
287520ad7b |
|||
|
2eab5749ea |
|||
|
e08763c526 |
|||
|
c420e19f08 |
|||
|
f7e13976e0 |
|||
|
db09718a96 |
|||
|
11e2c469cb |
|||
|
4569abc351 |
|||
|
dd203f6f31 |
|||
|
9f012b01e4 |
|||
|
e8e2bff5e4 |
|||
|
4451fc247e |
|||
|
0a13d27d6f |
|||
|
1137d2a07e |
|||
|
ad15b36f4d |
|||
|
24cb6d61aa |
|||
|
a342b432d4 |
|||
|
a5f146125b |
|||
|
4dd4977dd9 |
|||
| d31a59cbbe | |||
|
51780523b4 |
|||
|
9e788e0fe8 |
|||
|
76db869af5 |
|||
|
d3d6913474 |
|||
| 69e11d0799 | |||
|
6b4607e4f9 |
|||
| aef76e023a | |||
| 4409e42029 | |||
| 6183a065e9 | |||
| 0ef822de0c | |||
| 07db68882b | |||
| 13786584a2 | |||
| 6d867fcc71 | |||
| 7dd85a3988 | |||
| 863db8e491 | |||
|
b7d7db60e7 |
|||
|
0336744322 |
|||
|
7b13f91cc4 |
|||
|
f689128afc |
|||
|
2e624d9565 |
|||
|
d60117cd1d |
|||
|
fabe9d662b |
|||
|
507bac08f2 |
|||
|
c0802f8792 |
|||
|
c907bcde0c |
|||
|
784f4cb44a |
|||
|
528e981d7c |
|||
|
5f8d6ca882 |
|||
|
d56306e57b |
|||
|
43c654d9e7 |
|||
|
a7c8e58abd |
|||
|
b40f10cada |
|||
|
deba175f8f |
|||
|
c0c8e5262b |
|||
|
469d11ca7a |
|||
|
6ff247d58b |
|||
|
d5bac002c4 |
|||
|
481d9e22bb |
|||
|
335acb130e |
|||
|
f375e4217f |
|||
|
3dfe99aac7 |
|||
|
577d821709 |
|||
|
66f4f856a6 |
|||
|
73bc0b5338 |
|||
|
2f40e83568 |
|||
|
b2ef970810 |
|||
|
2b2826212f |
|||
|
94bb8c204b |
|||
|
87d49bddde |
|||
|
9383ed8d0b |
|||
|
2962675f9d |
|||
|
8532ee36d3 |
|||
|
22447b6027 |
|||
|
fcf0b4a778 |
|||
|
cc782bc39a |
|||
|
054bba1da9 |
|||
|
22e3eaab23 |
|||
|
e0efd19ef9 |
|||
|
0cd6f81a09 |
|||
|
30c9b2e248 |
|||
|
52ac67ba7a |
|||
|
272a5ea2fd |
|||
|
0bea4a1e95 |
|||
|
8ec738a765 |
|||
|
2f548725db |
|||
|
f6546e2335 |
|||
|
d52b8a3e83 |
|||
|
46133be181 |
|||
|
0faf896e8f |
|||
|
374e231a6b |
|||
|
729c52df2f |
|||
|
ac19c02e76 |
|||
|
0289f9f0b0 |
|||
|
14a10472a6 |
|||
|
b74bdd01ef |
|||
|
9c059f822f |
|||
|
75e369992d |
|||
|
a18f7b2d03 |
|||
|
7d1738964a |
|||
|
77004ec7d4 |
|||
|
3f891b580c |
|||
|
38f3244e31 |
|||
|
650d54292e |
|||
|
c40548be73 |
169 changed files with 5995 additions and 5218 deletions
|
|
@ -70,11 +70,6 @@ ADMIN_USER=admin# pick something better, this is used for `/admin`
|
|||
ADMIN_PASS=password
|
||||
DISPLAY_NAME='Joe Bloggs'# This is used for example in the header and titles
|
||||
|
||||
TWITTER_CONSUMER_KEY=
|
||||
TWITTER_CONSUMER_SECRET=
|
||||
TWITTER_ACCESS_TOKEN=
|
||||
TWITTER_ACCESS_TOKEN_SECRET=
|
||||
|
||||
SCOUT_DRIVER=database
|
||||
SCOUT_QUEUE=false
|
||||
|
||||
|
|
@ -83,8 +78,8 @@ SESSION_SAME_SITE=strict
|
|||
|
||||
LOG_SLACK_WEBHOOK_URL=
|
||||
|
||||
BRRR_WEBHOOK_URL=
|
||||
|
||||
FLARE_KEY=
|
||||
|
||||
IGNITION_OPEN_AI_KEY=
|
||||
|
||||
BRIDGY_MASTODON_TOKEN=
|
||||
|
|
|
|||
101
app/Console/Commands/MigrateMedia.php
Normal file
101
app/Console/Commands/MigrateMedia.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Media;
|
||||
use App\Models\Note;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class MigrateMedia extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:migrate-media';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Migrate media';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
// First check new `media_note` table exists
|
||||
if (! DB::getSchemaBuilder()->hasTable('media_note')) {
|
||||
$this->error('The table "media_note" does not exist.');
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Load all media already saved in `media_endpoint` table
|
||||
$this->line('Updating existing local media');
|
||||
$mediaEndpointMedia = Media::all();
|
||||
// Save relationship in new `media_note` table based on `media_endpoint.note_id`
|
||||
$this->withProgressBar($mediaEndpointMedia, function (Media $mediaEndpointMediaItem) {
|
||||
$note = Note::find($mediaEndpointMediaItem->note_id);
|
||||
if ($note) {
|
||||
$note->media()->syncWithoutDetaching($mediaEndpointMediaItem->id);
|
||||
}
|
||||
});
|
||||
|
||||
// Load all media records from `media` table
|
||||
$this->line('');
|
||||
$this->line('Migrating old media from S3');
|
||||
$oldMedia = DB::table('media')->get();
|
||||
foreach ($oldMedia as $oldMediaItem) {
|
||||
// We only want to process the S3 media
|
||||
if ($oldMediaItem->disk !== 's3') {
|
||||
$this->warn('Original media item never stored in S3');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check media exists in S3
|
||||
if (! Storage::disk('s3')->exists($oldMediaItem->id.DIRECTORY_SEPARATOR.$oldMediaItem->file_name)) {
|
||||
$this->warn('Original media item not found in S3');
|
||||
|
||||
continue;
|
||||
}
|
||||
// We want to just copy the file, check it does not already exist locally
|
||||
if (Storage::disk('public')->exists('media'.DIRECTORY_SEPARATOR.$oldMediaItem->file_name)) {
|
||||
$this->warn('File already exists locally with filename of original media item');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Save relationship based on `media.model_id`
|
||||
// I have already checked they are all notes
|
||||
$noteId = $oldMediaItem->model_id;
|
||||
$note = Note::find($noteId);
|
||||
if (! $note) {
|
||||
$this->warn('Note no longer exists');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create media entry in database and attach to note
|
||||
$newMediaItem = Media::create([
|
||||
'path' => 'media'.DIRECTORY_SEPARATOR.$oldMediaItem->file_name,
|
||||
'type' => 'image',
|
||||
]);
|
||||
$note->media()->syncWithoutDetaching($newMediaItem->id);
|
||||
|
||||
// Copy the file
|
||||
Storage::disk('public')->writeStream('media'.DIRECTORY_SEPARATOR.$oldMediaItem->file_name, Storage::disk('s3')->readStream($oldMediaItem->id.DIRECTORY_SEPARATOR.$oldMediaItem->file_name));
|
||||
|
||||
$this->info('Media item migrated from S3');
|
||||
}
|
||||
|
||||
$this->line('');
|
||||
$this->line('Migration finished');
|
||||
}
|
||||
}
|
||||
68
app/Console/Commands/ReprocessMediaImages.php
Normal file
68
app/Console/Commands/ReprocessMediaImages.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Media;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Image\ImageException;
|
||||
use Illuminate\Support\Facades\Image;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ReprocessMediaImages extends Command
|
||||
{
|
||||
protected $signature = 'app:reprocess-media-images {--dry-run : List images that would be processed without making changes}';
|
||||
|
||||
protected $description = 'Regenerate medium and small image variants using correct aspect-ratio scaling';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$media = Media::where('type', 'image')
|
||||
->whereNotNull('image_widths')
|
||||
->where('image_widths', '>', 1000)
|
||||
->get();
|
||||
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
$this->info("Found {$media->count()} images to reprocess.".($dryRun ? ' (dry run)' : ''));
|
||||
|
||||
foreach ($media as $item) {
|
||||
$path = $item->path;
|
||||
|
||||
if (! Storage::disk('public')->exists($path)) {
|
||||
$this->warn("{$path}: original not found on public disk, skipping.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->line("{$path} ({$item->image_widths}px wide)");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->info("Processing: {$path}");
|
||||
|
||||
$image = Image::fromStorage($path, 'public');
|
||||
try {
|
||||
$image->width();
|
||||
} catch (ImageException) {
|
||||
$this->warn(' Could not decode image, skipping.');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$filenameParts = explode('.', $path);
|
||||
$extension = array_pop($filenameParts);
|
||||
$basename = trim(implode('.', $filenameParts), '.');
|
||||
|
||||
Storage::disk('public')->put($basename.'-medium.'.$extension, $image->scale(width: 1000)->toBytes());
|
||||
Storage::disk('public')->put($basename.'-small.'.$extension, $image->scale(width: 500)->toBytes());
|
||||
|
||||
$this->info(' Done.');
|
||||
}
|
||||
|
||||
$this->info('Reprocessing complete.'.($dryRun ? ' (dry run — no files were changed)' : ''));
|
||||
}
|
||||
}
|
||||
7
app/Exceptions/MicropubUnsupportedModelException.php
Normal file
7
app/Exceptions/MicropubUnsupportedModelException.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class MicropubUnsupportedModelException extends \Exception {}
|
||||
|
|
@ -6,10 +6,11 @@ namespace App\Http\Controllers\Admin;
|
|||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Contact;
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ContactsController extends Controller
|
||||
|
|
@ -103,7 +104,7 @@ class ContactsController extends Controller
|
|||
* This method attempts to find the microformat marked-up profile image
|
||||
* from a given homepage and save it accordingly
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
* @return RedirectResponse|View
|
||||
*/
|
||||
public function getAvatar(int $contactId)
|
||||
{
|
||||
|
|
@ -112,14 +113,13 @@ class ContactsController extends Controller
|
|||
$avatar = null;
|
||||
$contact = Contact::findOrFail($contactId);
|
||||
if ($contact->homepage !== null && mb_strlen($contact->homepage) !== 0) {
|
||||
$client = resolve(Client::class);
|
||||
try {
|
||||
$response = $client->get($contact->homepage);
|
||||
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
|
||||
$response = Http::throw()->get($contact->homepage);
|
||||
} catch (RequestException $e) {
|
||||
return redirect('/admin/contacts/'.$contactId.'/edit')
|
||||
->with('error', 'Bad resposne from contact’s homepage');
|
||||
}
|
||||
$mf2 = \Mf2\parse((string) $response->getBody(), $contact->homepage);
|
||||
$mf2 = \Mf2\parse($response->body(), $contact->homepage);
|
||||
foreach ($mf2['items'] as $microformat) {
|
||||
if (Arr::get($microformat, 'type.0') === 'h-card') {
|
||||
$avatarURL = Arr::get($microformat, 'properties.photo.0.value');
|
||||
|
|
@ -128,8 +128,8 @@ class ContactsController extends Controller
|
|||
}
|
||||
if ($avatarURL !== null) {
|
||||
try {
|
||||
$avatar = $client->get($avatarURL);
|
||||
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
|
||||
$avatar = Http::throw()->get($avatarURL);
|
||||
} catch (RequestException $e) {
|
||||
return redirect('/admin/contacts/'.$contactId.'/edit')
|
||||
->with('error', 'Unable to download avatar');
|
||||
}
|
||||
|
|
@ -140,7 +140,7 @@ class ContactsController extends Controller
|
|||
if ($filesystem->isDirectory($directory) === false) {
|
||||
$filesystem->makeDirectory($directory);
|
||||
}
|
||||
$filesystem->put($directory . '/image', $avatar->getBody());
|
||||
$filesystem->put($directory.'/image', $avatar->body());
|
||||
|
||||
return view('admin.contacts.getavatarsuccess', [
|
||||
'homepage' => parse_url($contact->homepage, PHP_URL_HOST),
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ class PasskeysController extends Controller
|
|||
// Unset session data to mitigate replay attacks
|
||||
$request->session()->forget('create_options');
|
||||
if (empty($publicKeyCredentialCreationOptionsData)) {
|
||||
throw new WebAuthnException('No public key credential request options found');
|
||||
throw new WebauthnException('No public key credential request options found');
|
||||
}
|
||||
|
||||
$attestationStatementSupportManager = new AttestationStatementSupportManager;
|
||||
|
|
@ -145,7 +145,7 @@ class PasskeysController extends Controller
|
|||
);
|
||||
|
||||
if (! $publicKeyCredential->response instanceof AuthenticatorAttestationResponse) {
|
||||
throw new WebAuthnException('Invalid response type');
|
||||
throw new WebauthnException('Invalid response type');
|
||||
}
|
||||
|
||||
$algorithmManager = new Manager;
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ class PlacesController extends Controller
|
|||
foreach ($place1->notes as $note) {
|
||||
$note->place()->dissociate();
|
||||
$note->place()->associate($place2->id);
|
||||
$note->save();
|
||||
}
|
||||
$place1->delete();
|
||||
}
|
||||
|
|
@ -127,6 +128,7 @@ class PlacesController extends Controller
|
|||
foreach ($place2->notes as $note) {
|
||||
$note->place()->dissociate();
|
||||
$note->place()->associate($place1->id);
|
||||
$note->save();
|
||||
}
|
||||
$place2->delete();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,12 @@ declare(strict_types=1);
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\TokenService;
|
||||
use Exception;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\View\View;
|
||||
use Random\RandomException;
|
||||
|
|
@ -199,15 +198,13 @@ class IndieAuthController extends Controller
|
|||
}
|
||||
|
||||
// Otherwise we need to check the redirect_uri is in the client_id's redirect_uris
|
||||
$guzzle = resolve(Client::class);
|
||||
|
||||
try {
|
||||
$clientInfo = $guzzle->get($clientId);
|
||||
} catch (Exception) {
|
||||
$clientInfo = Http::throw()->get($clientId);
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$clientInfoParsed = \Mf2\parse($clientInfo->getBody()->getContents(), $clientId);
|
||||
$clientInfoParsed = \Mf2\parse($clientInfo->body(), $clientId);
|
||||
|
||||
$redirectUris = $clientInfoParsed['rels']['redirect_uri'] ?? [];
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,13 @@ namespace App\Http\Controllers;
|
|||
|
||||
use App\Exceptions\InvalidTokenScopeException;
|
||||
use App\Exceptions\MicropubHandlerException;
|
||||
use App\Exceptions\MicropubUnsupportedModelException;
|
||||
use App\Http\Requests\MicropubRequest;
|
||||
use App\Models\Place;
|
||||
use App\Models\SyndicationTarget;
|
||||
use App\Services\Micropub\Data\MicropubData;
|
||||
use App\Services\Micropub\MicropubHandlerRegistry;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Lcobucci\JWT\Token;
|
||||
|
|
@ -26,9 +29,9 @@ class MicropubController extends Controller
|
|||
/**
|
||||
* Respond to a POST request to the micropub endpoint.
|
||||
*
|
||||
* The request is initially processed by the MicropubRequest form request
|
||||
* class. The normalizes the data, so we can pass it into the handlers for
|
||||
* the different micropub requests, h-entry or h-card, for example.
|
||||
* MicropubRequest detects the request type (e.g. entry, card, update).
|
||||
* The handler registry resolves the appropriate handler, whose DTO
|
||||
* extracts the relevant fields from the request via fromRequest().
|
||||
*/
|
||||
public function post(MicropubRequest $request): JsonResponse
|
||||
{
|
||||
|
|
@ -43,13 +46,36 @@ class MicropubController extends Controller
|
|||
|
||||
try {
|
||||
$handler = $this->handlerRegistry->getHandler($type);
|
||||
$result = $handler->handle($request->getMicropubData());
|
||||
$dataClass = $handler->dataClass();
|
||||
/** @var MicropubData $data */
|
||||
$data = $dataClass::fromRequest($request);
|
||||
$result = $handler->handle($data);
|
||||
|
||||
if ($result['response'] === 'updated') {
|
||||
return response()->json([
|
||||
'response' => $result['response'],
|
||||
], 200)->header('Location', $result['url']);
|
||||
}
|
||||
|
||||
// Return appropriate response based on the handler result
|
||||
return response()->json([
|
||||
'response' => $result['response'],
|
||||
'location' => $result['url'] ?? null,
|
||||
], 201)->header('Location', $result['url']);
|
||||
} catch (InvalidTokenScopeException) {
|
||||
return response()->json([
|
||||
'error' => 'insufficient_scope',
|
||||
'error_description' => 'The token does not have the required scope for this request',
|
||||
], 401);
|
||||
} catch (ModelNotFoundException) {
|
||||
return response()->json([
|
||||
'error' => 'invalid_request',
|
||||
'error_description' => 'No known note with given ID',
|
||||
], 404);
|
||||
} catch (MicropubUnsupportedModelException) {
|
||||
return response()->json([
|
||||
'error' => 'invalid',
|
||||
'error_description' => 'This implementation currently only supports the updating of notes',
|
||||
], 500);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return response()->json([
|
||||
'error' => 'invalid_request',
|
||||
|
|
@ -57,15 +83,10 @@ class MicropubController extends Controller
|
|||
], 400);
|
||||
} catch (MicropubHandlerException) {
|
||||
return response()->json([
|
||||
'error' => 'Unknown Micropub type',
|
||||
'error' => 'unsupported_operation',
|
||||
'error_description' => 'The request could not be processed by this server',
|
||||
], 500);
|
||||
} catch (InvalidTokenScopeException) {
|
||||
return response()->json([
|
||||
'error' => 'invalid_scope',
|
||||
'error_description' => 'The token does not have the required scope for this request',
|
||||
], 403);
|
||||
} catch (\Exception) {
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'error' => 'server_error',
|
||||
'error_description' => 'An error occurred processing the request',
|
||||
|
|
@ -76,10 +97,9 @@ class MicropubController extends Controller
|
|||
/**
|
||||
* Respond to a GET request to the micropub endpoint.
|
||||
*
|
||||
* A GET request has been made to `api/post` with an accompanying
|
||||
* token, here we check whether the token is valid and respond
|
||||
* appropriately. Further if the request has the query parameter
|
||||
* syndicate-to we respond with the known syndication endpoints.
|
||||
* Token validation is handled by the VerifyMicropubToken middleware.
|
||||
* Supports q=syndicate-to, q=config, and q=geo:<lat>,<lng> queries.
|
||||
* The default response returns the token metadata.
|
||||
*/
|
||||
public function get(Request $request): JsonResponse
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,9 +13,10 @@ use Illuminate\Http\JsonResponse;
|
|||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Image\ImageException;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Image;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class MicropubMediaController extends Controller
|
||||
|
|
@ -111,12 +112,9 @@ class MicropubMediaController extends Controller
|
|||
|
||||
$filename = Storage::disk('local')->putFile('media', $file);
|
||||
|
||||
/** @var ImageManager $manager */
|
||||
$manager = resolve(ImageManager::class);
|
||||
try {
|
||||
$image = $manager->read($request->file('file'));
|
||||
$width = $image->width();
|
||||
} catch (Exception) {
|
||||
$width = Image::fromUpload($request->file('file'))->width();
|
||||
} catch (ImageException) {
|
||||
// not an image
|
||||
$width = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Note;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Response;
|
||||
|
|
@ -53,7 +52,7 @@ class NotesController extends Controller
|
|||
->withCount(['webmentions AS reposts' => function ($query) {
|
||||
$query->where('type', 'repost-of');
|
||||
}])->firstOrFail();
|
||||
} catch (ModelNotFoundException $exception) {
|
||||
} catch (\Exception) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class RedirectIfAuthenticated
|
|||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, string ...$guards): Response
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,12 +5,9 @@ declare(strict_types=1);
|
|||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class MicropubRequest extends FormRequest
|
||||
{
|
||||
protected array $micropubData = [];
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
|
|
@ -18,102 +15,25 @@ class MicropubRequest extends FormRequest
|
|||
];
|
||||
}
|
||||
|
||||
public function getMicropubData(): array
|
||||
{
|
||||
return $this->micropubData;
|
||||
}
|
||||
|
||||
public function getType(): ?string
|
||||
{
|
||||
// Return consistent type regardless of input format
|
||||
return $this->micropubData['type'] ?? null;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
// Normalize the request data based on content type
|
||||
if ($this->isJson()) {
|
||||
$this->normalizeMicropubJson();
|
||||
} else {
|
||||
$this->normalizeMicropubForm();
|
||||
}
|
||||
$data = $this->json()->all();
|
||||
|
||||
if (isset($data['action']) && $data['action'] === 'update') {
|
||||
return 'update';
|
||||
}
|
||||
|
||||
private function normalizeMicropubJson(): void
|
||||
{
|
||||
$json = $this->json();
|
||||
if ($json === null) {
|
||||
throw new \InvalidArgumentException('`isJson()` passed but there is no json data');
|
||||
}
|
||||
|
||||
$data = $json->all();
|
||||
|
||||
// Convert JSON type (h-entry) to simple type (entry)
|
||||
if (isset($data['type']) && is_array($data['type'])) {
|
||||
$type = current($data['type']);
|
||||
if (str_starts_with($type, 'h-')) {
|
||||
$this->micropubData['type'] = substr($type, 2);
|
||||
}
|
||||
}
|
||||
// Or set the type to update
|
||||
elseif (isset($data['action']) && $data['action'] === 'update') {
|
||||
$this->micropubData['type'] = 'update';
|
||||
}
|
||||
|
||||
// Add in the token data
|
||||
$this->micropubData['token_data'] = $data['token_data'];
|
||||
|
||||
// Add h-entry values
|
||||
$this->micropubData['content'] = Arr::get($data, 'properties.content.0');
|
||||
$this->micropubData['in-reply-to'] = Arr::get($data, 'properties.in-reply-to.0');
|
||||
$this->micropubData['published'] = Arr::get($data, 'properties.published.0');
|
||||
$this->micropubData['location'] = $this->getLocationData($data);
|
||||
$this->micropubData['bookmark-of'] = Arr::get($data, 'properties.bookmark-of.0');
|
||||
$this->micropubData['like-of'] = Arr::get($data, 'properties.like-of.0');
|
||||
$this->micropubData['mp-syndicate-to'] = Arr::get($data, 'properties.mp-syndicate-to');
|
||||
|
||||
// Add h-card values
|
||||
$this->micropubData['name'] = Arr::get($data, 'properties.name.0');
|
||||
$this->micropubData['description'] = Arr::get($data, 'properties.description.0');
|
||||
$this->micropubData['geo'] = Arr::get($data, 'properties.geo.0');
|
||||
|
||||
// Add checkin value
|
||||
$this->micropubData['checkin'] = Arr::get($data, 'checkin');
|
||||
$this->micropubData['syndication'] = Arr::get($data, 'properties.syndication.0');
|
||||
}
|
||||
|
||||
private function normalizeMicropubForm(): void
|
||||
{
|
||||
// Convert form h=entry to type=entry
|
||||
if ($h = $this->input('h')) {
|
||||
$this->micropubData['type'] = $h;
|
||||
}
|
||||
|
||||
// Add some fields to the micropub data with default null values
|
||||
$this->micropubData['in-reply-to'] = null;
|
||||
$this->micropubData['published'] = null;
|
||||
$this->micropubData['location'] = null;
|
||||
$this->micropubData['description'] = null;
|
||||
$this->micropubData['geo'] = null;
|
||||
$this->micropubData['latitude'] = null;
|
||||
$this->micropubData['longitude'] = null;
|
||||
|
||||
// Map form fields to micropub data
|
||||
foreach ($this->except(['h', 'access_token']) as $key => $value) {
|
||||
$this->micropubData[$key] = $value;
|
||||
return substr($type, 2);
|
||||
}
|
||||
}
|
||||
|
||||
private function getLocationData(array $data): array|string|null
|
||||
{
|
||||
if (! Arr::has($data, 'properties.location')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Arr::has($data, 'properties.location.0')) {
|
||||
return Arr::get($data, 'properties.location.0');
|
||||
}
|
||||
|
||||
return Arr::get($data, 'properties.location');
|
||||
return $this->input('h') ?: null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Jobs;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\FileSystem\FileSystem;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class DownloadWebMention implements ShouldQueue
|
||||
{
|
||||
|
|
@ -29,15 +29,15 @@ class DownloadWebMention implements ShouldQueue
|
|||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
* @throws RequestException
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function handle(Client $guzzle): void
|
||||
public function handle(): void
|
||||
{
|
||||
$response = $guzzle->request('GET', $this->source);
|
||||
// 4XX and 5XX responses should get Guzzle to throw an exception,
|
||||
// Laravel should catch and retry these automatically.
|
||||
if ($response->getStatusCode() === 200) {
|
||||
// 4XX and 5XX responses should throw so Laravel can catch and
|
||||
// retry these automatically.
|
||||
$response = Http::throw()->get($this->source);
|
||||
if ($response->status() === 200) {
|
||||
$filesystem = new FileSystem;
|
||||
$filename = storage_path('HTML').'/'.$this->createFilenameFromURL($this->source);
|
||||
// backup file first
|
||||
|
|
@ -56,7 +56,7 @@ class DownloadWebMention implements ShouldQueue
|
|||
// save new HTML
|
||||
$filesystem->put(
|
||||
$filename,
|
||||
(string) $response->getBody()
|
||||
$response->body()
|
||||
);
|
||||
// remove backup if the same
|
||||
if ($filesystem->exists($filenameBackup)) {
|
||||
|
|
|
|||
56
app/Jobs/NotifyBrrrOfWebMention.php
Normal file
56
app/Jobs/NotifyBrrrOfWebMention.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\WebMention;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class NotifyBrrrOfWebMention implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
protected WebMention $webMention
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$webhookUrl = config('services.brrr.webhook_url');
|
||||
|
||||
if (blank($webhookUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Http::post($webhookUrl, [
|
||||
'title' => $this->title(),
|
||||
'message' => "From {$this->webMention->source}",
|
||||
'open_url' => $this->webMention->target,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a notification title based on the webmention type.
|
||||
*/
|
||||
private function title(): string
|
||||
{
|
||||
return match ($this->webMention->type) {
|
||||
'in-reply-to' => 'New reply',
|
||||
'like-of' => 'New like',
|
||||
'repost-of' => 'New repost',
|
||||
default => 'New webmention',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -5,16 +5,13 @@ declare(strict_types=1);
|
|||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Like;
|
||||
use Codebird\Codebird;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Jonnybarnes\WebmentionsParser\Authorship;
|
||||
use Jonnybarnes\WebmentionsParser\Exceptions\AuthorshipParserException;
|
||||
|
||||
|
|
@ -34,42 +31,11 @@ class ProcessLike implements ShouldQueue
|
|||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function handle(Client $client, Authorship $authorship): int
|
||||
public function handle(Authorship $authorship): int
|
||||
{
|
||||
if ($this->isTweet($this->like->url)) {
|
||||
$codebird = resolve(Codebird::class);
|
||||
|
||||
$tweet = $codebird->statuses_oembed(['url' => $this->like->url]);
|
||||
|
||||
$this->like->author_name = $tweet->author_name;
|
||||
$this->like->author_url = $tweet->author_url;
|
||||
$this->like->content = $tweet->html;
|
||||
$this->like->save();
|
||||
|
||||
// POSSE like
|
||||
try {
|
||||
$client->request(
|
||||
'POST',
|
||||
'https://brid.gy/publish/webmention',
|
||||
[
|
||||
'form_params' => [
|
||||
'source' => $this->like->url,
|
||||
'target' => 'https://brid.gy/publish/twitter',
|
||||
],
|
||||
]
|
||||
);
|
||||
} catch (RequestException) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$response = $client->request('GET', $this->like->url);
|
||||
$mf2 = \Mf2\parse((string) $response->getBody(), $this->like->url);
|
||||
$response = Http::throw()->get($this->like->url);
|
||||
$mf2 = \Mf2\parse($response->body(), $this->like->url);
|
||||
if (Arr::has($mf2, 'items.0.properties.content')) {
|
||||
$this->like->content = $mf2['items'][0]['properties']['content'][0]['html'];
|
||||
}
|
||||
|
|
@ -91,15 +57,4 @@ class ProcessLike implements ShouldQueue
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a given URL is that of a Tweet.
|
||||
*/
|
||||
private function isTweet(string $url): bool
|
||||
{
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
$parts = array_reverse(explode('.', $host));
|
||||
|
||||
return $parts[0] === 'com' && $parts[1] === 'twitter';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ namespace App\Jobs;
|
|||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Image\ImageException;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Image;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\Exceptions\DecoderException;
|
||||
use Intervention\Image\ImageManager;
|
||||
|
||||
class ProcessMedia implements ShouldQueue
|
||||
{
|
||||
|
|
@ -30,40 +30,38 @@ class ProcessMedia implements ShouldQueue
|
|||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(ImageManager $manager): void
|
||||
public function handle(): void
|
||||
{
|
||||
// Load file
|
||||
$file = Storage::disk('local')->get('media/' . $this->filename);
|
||||
$file = Storage::disk('local')->get($this->filename);
|
||||
|
||||
// Open file
|
||||
$image = Image::fromStorage($this->filename, 'local');
|
||||
try {
|
||||
$image = $manager->read($file);
|
||||
} catch (DecoderException) {
|
||||
$width = $image->width();
|
||||
} catch (ImageException) {
|
||||
// not an image; delete file and end job
|
||||
Storage::disk('local')->delete('media/' . $this->filename);
|
||||
Storage::disk('local')->delete($this->filename);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the file publicly
|
||||
Storage::disk('public')->put('media/' . $this->filename, $file);
|
||||
Storage::disk('public')->put($this->filename, $file);
|
||||
|
||||
// Create smaller versions if necessary
|
||||
if ($image->width() > 1000) {
|
||||
if ($width > 1000) {
|
||||
$filenameParts = explode('.', $this->filename);
|
||||
$extension = array_pop($filenameParts);
|
||||
// the following achieves this data flow
|
||||
// foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar
|
||||
$basename = trim(implode('.', $filenameParts), '.');
|
||||
|
||||
$medium = $image->resize(width: 1000);
|
||||
Storage::disk('public')->put('media/' . $basename . '-medium.' . $extension, (string) $medium->encode());
|
||||
|
||||
$small = $image->resize(width: 500);
|
||||
Storage::disk('public')->put('media/' . $basename . '-small.' . $extension, (string) $small->encode());
|
||||
Storage::disk('public')->put($basename.'-medium.'.$extension, $image->scale(width: 1000)->toBytes());
|
||||
Storage::disk('public')->put($basename.'-small.'.$extension, $image->scale(width: 500)->toBytes());
|
||||
}
|
||||
|
||||
// Now we can delete the locally saved image
|
||||
Storage::disk('local')->delete('media/' . $this->filename);
|
||||
Storage::disk('local')->delete($this->filename);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,12 @@ namespace App\Jobs;
|
|||
use App\Exceptions\RemoteContentNotFoundException;
|
||||
use App\Models\Note;
|
||||
use App\Models\WebMention;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Jonnybarnes\WebmentionsParser\Exceptions\InvalidMentionException;
|
||||
use Jonnybarnes\WebmentionsParser\Parser;
|
||||
use Mf2;
|
||||
|
|
@ -36,18 +35,20 @@ class ProcessWebMention implements ShouldQueue
|
|||
* Execute the job.
|
||||
*
|
||||
* @throws RemoteContentNotFoundException
|
||||
* @throws GuzzleException
|
||||
* @throws InvalidMentionException
|
||||
*/
|
||||
public function handle(Parser $parser, Client $guzzle): void
|
||||
public function handle(Parser $parser): void
|
||||
{
|
||||
try {
|
||||
$response = $guzzle->request('GET', $this->source);
|
||||
} catch (RequestException $e) {
|
||||
$response = Http::get($this->source);
|
||||
} catch (ConnectionException) {
|
||||
throw new RemoteContentNotFoundException;
|
||||
}
|
||||
$this->saveRemoteContent((string) $response->getBody(), $this->source);
|
||||
$microformats = Mf2\parse((string) $response->getBody(), $this->source);
|
||||
if ($response->failed()) {
|
||||
throw new RemoteContentNotFoundException;
|
||||
}
|
||||
$this->saveRemoteContent($response->body(), $this->source);
|
||||
$microformats = Mf2\parse($response->body(), $this->source);
|
||||
$webmentions = WebMention::where('source', $this->source)->get();
|
||||
foreach ($webmentions as $webmention) {
|
||||
// check webmention still references target
|
||||
|
|
@ -95,6 +96,7 @@ class ProcessWebMention implements ShouldQueue
|
|||
$webmention->type = $type;
|
||||
$webmention->mf2 = json_encode($microformats);
|
||||
$webmention->save();
|
||||
dispatch(new NotifyBrrrOfWebMention($webmention));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Jobs;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Jonnybarnes\WebmentionsParser\Authorship;
|
||||
use Jonnybarnes\WebmentionsParser\Exceptions\AuthorshipParserException;
|
||||
|
||||
|
|
@ -55,12 +56,10 @@ class SaveProfileImage implements ShouldQueue
|
|||
&& parse_url($photo, PHP_URL_HOST) !== 'pbs.twimg.com'
|
||||
&& parse_url($photo, PHP_URL_HOST) !== 'twitter.com'
|
||||
) {
|
||||
$client = resolve(Client::class);
|
||||
|
||||
try {
|
||||
$response = $client->get($photo);
|
||||
$image = $response->getBody();
|
||||
} catch (RequestException) {
|
||||
$response = Http::throw()->get($photo);
|
||||
$image = $response->body();
|
||||
} catch (ConnectionException|RequestException) {
|
||||
// we are opening and reading the default image so that
|
||||
$default = public_path().'/assets/profile-images/default-image';
|
||||
$handle = fopen($default, 'rb');
|
||||
|
|
|
|||
|
|
@ -5,14 +5,15 @@ declare(strict_types=1);
|
|||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Bookmark;
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use JsonException;
|
||||
|
||||
class SaveScreenshot implements ShouldQueue
|
||||
{
|
||||
|
|
@ -27,77 +28,68 @@ class SaveScreenshot implements ShouldQueue
|
|||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
*
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
// A normal Guzzle client
|
||||
$client = resolve(Client::class);
|
||||
// A Guzzle client with a custom Middleware to retry the CloudConvert API requests
|
||||
$retryClient = resolve('RetryGuzzle');
|
||||
$cloudConvert = Http::baseUrl('https://api.cloudconvert.com/v2')
|
||||
->withToken(config('services.cloudconvert.token'))
|
||||
->throw();
|
||||
|
||||
// First request that CloudConvert takes a screenshot of the URL
|
||||
$takeScreenshotJobResponse = $client->request('POST', 'https://api.cloudconvert.com/v2/capture-website', [
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . config('services.cloudconvert.token'),
|
||||
],
|
||||
'json' => [
|
||||
$takeScreenshotJobResponse = $cloudConvert->post('/capture-website', [
|
||||
'url' => $this->bookmark->url,
|
||||
'output_format' => 'png',
|
||||
'screen_width' => 1440,
|
||||
'screen_height' => 900,
|
||||
'wait_until' => 'networkidle0',
|
||||
'wait_time' => 100,
|
||||
],
|
||||
]);
|
||||
|
||||
$taskId = json_decode($takeScreenshotJobResponse->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR)->data->id;
|
||||
$taskId = $takeScreenshotJobResponse->json('data.id');
|
||||
|
||||
// Now wait till the status job is finished
|
||||
$screenshotJobStatusResponse = $retryClient->request('GET', 'https://api.cloudconvert.com/v2/tasks/' . $taskId, [
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . config('services.cloudconvert.token'),
|
||||
],
|
||||
'query' => [
|
||||
'include' => 'payload',
|
||||
],
|
||||
]);
|
||||
$screenshotJobStatusResponse = $this->pollUntilFinished($cloudConvert, $taskId);
|
||||
|
||||
$finishedCaptureId = json_decode($screenshotJobStatusResponse->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR)->data->id;
|
||||
$finishedCaptureId = $screenshotJobStatusResponse->json('data.id');
|
||||
|
||||
// Now we can create a new job to request thst the screenshot is exported to a temporary URL we can download the screenshot from
|
||||
$exportImageJob = $client->request('POST', 'https://api.cloudconvert.com/v2/export/url', [
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . config('services.cloudconvert.token'),
|
||||
],
|
||||
'json' => [
|
||||
$exportImageJob = $cloudConvert->post('/export/url', [
|
||||
'input' => $finishedCaptureId,
|
||||
'archive_multiple_files' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
$exportImageJobId = json_decode($exportImageJob->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR)->data->id;
|
||||
$exportImageJobId = $exportImageJob->json('data.id');
|
||||
|
||||
// Again, wait till the status of this export job is finished
|
||||
$finalImageUrlResponse = $retryClient->request('GET', 'https://api.cloudconvert.com/v2/tasks/' . $exportImageJobId, [
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . config('services.cloudconvert.token'),
|
||||
],
|
||||
'query' => [
|
||||
'include' => 'payload',
|
||||
],
|
||||
]);
|
||||
$finalImageUrlResponse = $this->pollUntilFinished($cloudConvert, $exportImageJobId);
|
||||
|
||||
// Now we can download the screenshot and save it to the storage
|
||||
$finalImageUrl = json_decode($finalImageUrlResponse->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR)->data->result->files[0]->url;
|
||||
$finalImageUrl = $finalImageUrlResponse->json('data.result.files.0.url');
|
||||
|
||||
$finalImageUrlContent = $client->request('GET', $finalImageUrl);
|
||||
$finalImageUrlContent = Http::throw()->get($finalImageUrl);
|
||||
|
||||
Storage::disk('public')->put('/assets/img/bookmarks/' . $taskId . '.png', $finalImageUrlContent->getBody()->getContents());
|
||||
Storage::disk('public')->put('/assets/img/bookmarks/'.$taskId.'.png', $finalImageUrlContent->body());
|
||||
|
||||
$this->bookmark->screenshot = $taskId;
|
||||
$this->bookmark->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll a CloudConvert task until it reports a "finished" status.
|
||||
*/
|
||||
private function pollUntilFinished(PendingRequest $client, string $taskId): Response
|
||||
{
|
||||
$attempts = 0;
|
||||
|
||||
do {
|
||||
$response = $client->get('/tasks/'.$taskId, ['include' => 'payload']);
|
||||
$finished = $response->json('data.status') === 'finished';
|
||||
if (! $finished) {
|
||||
$attempts++;
|
||||
usleep(1_000_000); // 1 second, matches CloudConvert's own polling guidance
|
||||
}
|
||||
} while (! $finished && $attempts < 5);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ declare(strict_types=1);
|
|||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Note;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\Psr7\Header;
|
||||
use GuzzleHttp\Psr7\UriResolver;
|
||||
use GuzzleHttp\Psr7\Utils;
|
||||
|
|
@ -14,7 +12,9 @@ use Illuminate\Bus\Queueable;
|
|||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
use Mf2\Parser;
|
||||
|
||||
class SendWebMentions implements ShouldQueue
|
||||
{
|
||||
|
|
@ -31,8 +31,6 @@ class SendWebMentions implements ShouldQueue
|
|||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
|
|
@ -42,12 +40,9 @@ class SendWebMentions implements ShouldQueue
|
|||
foreach ($urls as $url) {
|
||||
$endpoint = $this->discoverWebmentionEndpoint($url);
|
||||
if ($endpoint !== null) {
|
||||
$guzzle = resolve(Client::class);
|
||||
$guzzle->post($endpoint, [
|
||||
'form_params' => [
|
||||
Http::asForm()->post($endpoint, [
|
||||
'source' => $this->note->uri,
|
||||
'target' => $url,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -55,8 +50,6 @@ class SendWebMentions implements ShouldQueue
|
|||
|
||||
/**
|
||||
* Discover if a URL has a webmention endpoint.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function discoverWebmentionEndpoint(string $url): ?string
|
||||
{
|
||||
|
|
@ -70,10 +63,9 @@ class SendWebMentions implements ShouldQueue
|
|||
|
||||
$endpoint = null;
|
||||
|
||||
$guzzle = resolve(Client::class);
|
||||
$response = $guzzle->get($url);
|
||||
$response = Http::get($url);
|
||||
// check HTTP Headers for webmention endpoint
|
||||
$links = Header::parse($response->getHeader('Link'));
|
||||
$links = Header::parse($response->header('Link'));
|
||||
foreach ($links as $link) {
|
||||
if (array_key_exists('rel', $link) && mb_stristr($link['rel'], 'webmention')) {
|
||||
return $this->resolveUri(trim($link[0], '<>'), $url);
|
||||
|
|
@ -81,9 +73,13 @@ class SendWebMentions implements ShouldQueue
|
|||
}
|
||||
|
||||
// failed to find a header so parse HTML
|
||||
$html = (string) $response->getBody();
|
||||
$html = $response->body();
|
||||
|
||||
$mf2 = new \Mf2\Parser($html, $url);
|
||||
if ($html === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mf2 = new Parser($html, $url);
|
||||
$rels = $mf2->parseRelsAndAlternates();
|
||||
if (array_key_exists('webmention', $rels[0])) {
|
||||
$endpoint = $rels[0]['webmention'][0];
|
||||
|
|
|
|||
|
|
@ -5,18 +5,22 @@ declare(strict_types=1);
|
|||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Note;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class SyndicateNoteToBluesky implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Only try once — retrying would send Bridgy a duplicate publish webmention.
|
||||
*/
|
||||
public int $tries = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
|
|
@ -26,37 +30,26 @@ class SyndicateNoteToBluesky implements ShouldQueue
|
|||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function handle(Client $guzzle): void
|
||||
public function handle(): void
|
||||
{
|
||||
// We can only make the request if we have an access token
|
||||
if (config('bridgy.bluesky_token') === null) {
|
||||
// no ->throw()/->retry() here — Bridgy would duplicate-publish on retry, see $tries above
|
||||
$response = Http::acceptJson()->asForm()->post('https://brid.gy/publish/webmention', [
|
||||
'source' => $this->note->uri,
|
||||
'target' => 'https://brid.gy/publish/bluesky',
|
||||
]);
|
||||
|
||||
$body = $response->json();
|
||||
|
||||
if ($response->status() === 201) {
|
||||
$this->note->bluesky_url = $body['url'];
|
||||
$this->note->save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Make micropub request
|
||||
$response = $guzzle->request(
|
||||
'POST',
|
||||
'https://brid.gy/micropub',
|
||||
[
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . config('bridgy.bluesky_token'),
|
||||
],
|
||||
'json' => [
|
||||
'type' => ['h-entry'],
|
||||
'properties' => [
|
||||
'content' => [$this->note->getRawOriginal('note')],
|
||||
],
|
||||
],
|
||||
]
|
||||
throw new \RuntimeException(
|
||||
'Bridgy publish to Bluesky failed: '.($body['error'] ?? $response->body())
|
||||
);
|
||||
|
||||
// Parse for syndication URL
|
||||
if ($response->getStatusCode() === 201) {
|
||||
$this->note->bluesky_url = $response->getHeader('Location')[0];
|
||||
$this->note->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,18 +5,22 @@ declare(strict_types=1);
|
|||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Note;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class SyndicateNoteToMastodon implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Only try once — retrying would send Bridgy a duplicate publish webmention.
|
||||
*/
|
||||
public int $tries = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
|
|
@ -26,38 +30,26 @@ class SyndicateNoteToMastodon implements ShouldQueue
|
|||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function handle(Client $guzzle): void
|
||||
public function handle(): void
|
||||
{
|
||||
// We can only make the request if we have an access token
|
||||
if (config('bridgy.mastodon_token') === null) {
|
||||
// no ->throw()/->retry() here — Bridgy would duplicate-publish on retry, see $tries above
|
||||
$response = Http::acceptJson()->asForm()->post('https://brid.gy/publish/webmention', [
|
||||
'source' => $this->note->uri,
|
||||
'target' => 'https://brid.gy/publish/mastodon',
|
||||
]);
|
||||
|
||||
$body = $response->json();
|
||||
|
||||
if ($response->status() === 201) {
|
||||
$this->note->mastodon_url = $body['url'];
|
||||
$this->note->save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Make micropub request
|
||||
$response = $guzzle->request(
|
||||
'POST',
|
||||
'https://brid.gy/micropub',
|
||||
[
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . config('bridgy.mastodon_token'),
|
||||
],
|
||||
'json' => [
|
||||
'type' => ['h-entry'],
|
||||
'properties' => [
|
||||
'content' => [$this->note->getRawOriginal('note')],
|
||||
],
|
||||
],
|
||||
]
|
||||
throw new \RuntimeException(
|
||||
'Bridgy publish to Mastodon failed: '.($body['error'] ?? $response->body())
|
||||
);
|
||||
|
||||
// Parse for syndication URL
|
||||
if ($response->getStatusCode() === 201) {
|
||||
$mastodonUrl = $response->getHeader('Location')[0];
|
||||
$this->note->mastodon_url = $mastodonUrl;
|
||||
$this->note->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ declare(strict_types=1);
|
|||
namespace App\Models;
|
||||
|
||||
use Cviebrock\EloquentSluggable\Sluggable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -12,29 +14,17 @@ use Illuminate\Database\Eloquent\Model;
|
|||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use League\CommonMark\Environment\Environment;
|
||||
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\IndentedCode;
|
||||
use League\CommonMark\MarkdownConverter;
|
||||
use Spatie\CommonMarkHighlighter\FencedCodeRenderer;
|
||||
use Spatie\CommonMarkHighlighter\IndentedCodeRenderer;
|
||||
use Tempest\Highlight\CommonMark\HighlightExtension;
|
||||
|
||||
#[Table('articles')]
|
||||
#[Fillable(['url', 'title', 'main', 'published'])]
|
||||
class Article extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use Sluggable;
|
||||
use SoftDeletes;
|
||||
|
||||
/** @var string */
|
||||
protected $table = 'articles';
|
||||
|
||||
/** @var array<int, string> */
|
||||
protected $fillable = [
|
||||
'url',
|
||||
'title',
|
||||
'main',
|
||||
'published',
|
||||
];
|
||||
|
||||
/** @var array<string, string> */
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
|
|
@ -60,8 +50,7 @@ class Article extends Model
|
|||
get: function () {
|
||||
$environment = new Environment;
|
||||
$environment->addExtension(new CommonMarkCoreExtension);
|
||||
$environment->addRenderer(FencedCode::class, new FencedCodeRenderer);
|
||||
$environment->addRenderer(IndentedCode::class, new IndentedCodeRenderer);
|
||||
$environment->addExtension(new HighlightExtension);
|
||||
$markdownConverter = new MarkdownConverter($environment);
|
||||
|
||||
return $markdownConverter->convert($this->main)->getContent();
|
||||
|
|
|
|||
|
|
@ -4,18 +4,17 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
#[Fillable(['url', 'name', 'content'])]
|
||||
class Bookmark extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/** @var array<int, string> */
|
||||
protected $fillable = ['url', 'name', 'content'];
|
||||
|
||||
/** @var array<string, string> */
|
||||
protected $casts = [
|
||||
'syndicates' => 'array',
|
||||
|
|
@ -26,7 +25,7 @@ class Bookmark extends Model
|
|||
return $this->belongsToMany('App\Models\Tag');
|
||||
}
|
||||
|
||||
protected function local_uri(): Attribute
|
||||
protected function localUri(): Attribute
|
||||
{
|
||||
return Attribute::get(
|
||||
get: fn () => config('app.url').'/bookmarks/'.$this->id,
|
||||
|
|
|
|||
|
|
@ -4,20 +4,18 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
#[Table('contacts')]
|
||||
#[Fillable(['nick', 'name', 'homepage', 'twitter', 'facebook'])]
|
||||
class Contact extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/** @var string */
|
||||
protected $table = 'contacts';
|
||||
|
||||
/** @var array<int, string> */
|
||||
protected $fillable = ['nick', 'name', 'homepage', 'twitter', 'facebook'];
|
||||
|
||||
protected function photo(): Attribute
|
||||
{
|
||||
$photo = '/assets/profile-images/default-image';
|
||||
|
|
|
|||
|
|
@ -5,20 +5,19 @@ declare(strict_types=1);
|
|||
namespace App\Models;
|
||||
|
||||
use App\Traits\FilterHtml;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
use Mf2;
|
||||
|
||||
#[Fillable(['url'])]
|
||||
class Like extends Model
|
||||
{
|
||||
use FilterHtml;
|
||||
use HasFactory;
|
||||
|
||||
/** @var array<int, string> */
|
||||
protected $fillable = ['url'];
|
||||
|
||||
protected function url(): Attribute
|
||||
{
|
||||
return Attribute::set(
|
||||
|
|
|
|||
|
|
@ -4,25 +4,25 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
#[Table('media_endpoint')]
|
||||
#[Fillable(['token', 'path', 'type', 'image_widths'])]
|
||||
class Media extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/** @var string */
|
||||
protected $table = 'media_endpoint';
|
||||
|
||||
/** @var array<int, string> */
|
||||
protected $fillable = ['token', 'path', 'type', 'image_widths'];
|
||||
|
||||
public function note(): BelongsTo
|
||||
public function notes(): BelongsToMany
|
||||
{
|
||||
return $this->belongsTo(Note::class);
|
||||
return $this->belongsToMany(Note::class)
|
||||
->withPivot('alt_text', 'order')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
protected function url(): Attribute
|
||||
|
|
|
|||
|
|
@ -4,20 +4,18 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Table('clients')]
|
||||
#[Fillable(['client_url', 'client_name'])]
|
||||
class MicropubClient extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/** @var string */
|
||||
protected $table = 'clients';
|
||||
|
||||
/** @var array<int, string> */
|
||||
protected $fillable = ['client_url', 'client_name'];
|
||||
|
||||
public function notes(): HasMany
|
||||
{
|
||||
return $this->hasMany('App\Models\Note', 'client_id', 'client_url');
|
||||
|
|
|
|||
|
|
@ -6,32 +6,35 @@ namespace App\Models;
|
|||
|
||||
use App\CommonMark\Generators\MentionGenerator;
|
||||
use App\CommonMark\Renderers\MentionRenderer;
|
||||
use Codebird\Codebird;
|
||||
use Exception;
|
||||
use GuzzleHttp\Client;
|
||||
use App\Observers\NoteObserver;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
|
||||
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Jonnybarnes\IndieWeb\Numbers;
|
||||
use Laravel\Scout\Searchable;
|
||||
use League\CommonMark\Environment\Environment;
|
||||
use League\CommonMark\Extension\Autolink\AutolinkExtension;
|
||||
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\IndentedCode;
|
||||
use League\CommonMark\Extension\Mention\Mention;
|
||||
use League\CommonMark\Extension\Mention\MentionExtension;
|
||||
use League\CommonMark\MarkdownConverter;
|
||||
use Normalizer;
|
||||
use Spatie\CommonMarkHighlighter\FencedCodeRenderer;
|
||||
use Spatie\CommonMarkHighlighter\IndentedCodeRenderer;
|
||||
use Tempest\Highlight\CommonMark\HighlightExtension;
|
||||
|
||||
#[Table('notes')]
|
||||
#[Fillable(['note', 'in_reply_to', 'client_id'])]
|
||||
#[Hidden(['searchable'])]
|
||||
#[ObservedBy(NoteObserver::class)]
|
||||
class Note extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
|
@ -62,16 +65,6 @@ class Note extends Model
|
|||
/** @var string */
|
||||
protected $table = 'notes';
|
||||
|
||||
/** @var array<int, string> */
|
||||
protected $fillable = [
|
||||
'note',
|
||||
'in_reply_to',
|
||||
'client_id',
|
||||
];
|
||||
|
||||
/** @var array<int, string> */
|
||||
protected $hidden = ['searchable'];
|
||||
|
||||
public function tags(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Tag::class);
|
||||
|
|
@ -92,9 +85,12 @@ class Note extends Model
|
|||
return $this->belongsTo(Place::class);
|
||||
}
|
||||
|
||||
public function media(): HasMany
|
||||
public function media(): BelongsToMany
|
||||
{
|
||||
return $this->hasMany(Media::class);
|
||||
return $this->BelongsToMany(Media::class)
|
||||
->withPivot('alt_text', 'order')
|
||||
->withTimestamps()
|
||||
->orderBy('order');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -239,43 +235,6 @@ class Note extends Model
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the OEmbed html for a tweet the note is a reply to.
|
||||
*/
|
||||
public function getTwitterAttribute(): ?object
|
||||
{
|
||||
if (
|
||||
$this->in_reply_to === null ||
|
||||
! $this->isTwitterLink($this->in_reply_to)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tweetId = basename($this->in_reply_to);
|
||||
if (Cache::has($tweetId)) {
|
||||
return Cache::get($tweetId);
|
||||
}
|
||||
|
||||
try {
|
||||
$codebird = resolve(Codebird::class);
|
||||
$oEmbed = $codebird->statuses_oembed([
|
||||
'url' => $this->in_reply_to,
|
||||
'dnt' => true,
|
||||
'align' => 'center',
|
||||
'maxwidth' => 512,
|
||||
]);
|
||||
|
||||
if ($oEmbed->httpstatus >= 400) {
|
||||
throw new Exception;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
Cache::put($tweetId, $oEmbed, ($oEmbed->cache_age));
|
||||
|
||||
return $oEmbed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to select a note via a NewBase60 id.
|
||||
*/
|
||||
|
|
@ -392,9 +351,8 @@ class Note extends Model
|
|||
$environment->addExtension(new CommonMarkCoreExtension);
|
||||
$environment->addExtension(new AutolinkExtension);
|
||||
$environment->addExtension(new MentionExtension);
|
||||
$environment->addExtension(new HighlightExtension);
|
||||
$environment->addRenderer(Mention::class, new MentionRenderer);
|
||||
$environment->addRenderer(FencedCode::class, new FencedCodeRenderer);
|
||||
$environment->addRenderer(IndentedCode::class, new IndentedCodeRenderer);
|
||||
$markdownConverter = new MarkdownConverter($environment);
|
||||
|
||||
return $markdownConverter->convert($note)->getContent();
|
||||
|
|
@ -405,18 +363,15 @@ class Note extends Model
|
|||
$latLng = $latitude.','.$longitude;
|
||||
|
||||
return Cache::get($latLng, function () use ($latLng, $latitude, $longitude) {
|
||||
$guzzle = resolve(Client::class);
|
||||
$response = $guzzle->request('GET', 'https://nominatim.openstreetmap.org/reverse', [
|
||||
'query' => [
|
||||
$response = Http::withHeaders(['User-Agent' => 'jonnybarnes.uk, email jonny@jonnybarnes.uk'])
|
||||
->get('https://nominatim.openstreetmap.org/reverse', [
|
||||
'format' => 'json',
|
||||
'lat' => $latitude,
|
||||
'lon' => $longitude,
|
||||
'zoom' => 18,
|
||||
'addressdetails' => 1,
|
||||
],
|
||||
'headers' => ['User-Agent' => 'jonnybarnes.uk via Guzzle, email jonny@jonnybarnes.uk'],
|
||||
]);
|
||||
$json = json_decode((string) $response->getBody());
|
||||
$json = $response->object();
|
||||
if (isset($json->address->suburb)) {
|
||||
$locality = $json->address->suburb;
|
||||
if (isset($json->address->city)) {
|
||||
|
|
@ -457,9 +412,4 @@ class Note extends Model
|
|||
return $address;
|
||||
});
|
||||
}
|
||||
|
||||
private function isTwitterLink(string $inReplyTo): bool
|
||||
{
|
||||
return str_starts_with($inReplyTo, 'https://twitter.com/');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,20 +4,16 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Fillable(['passkey_id', 'passkey'])]
|
||||
class Passkey extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/** @inerhitDoc */
|
||||
protected $fillable = [
|
||||
'passkey_id',
|
||||
'passkey',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace App\Models;
|
||||
|
||||
use Cviebrock\EloquentSluggable\Sluggable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -12,6 +13,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
#[Fillable(['name', 'slug'])]
|
||||
class Place extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
|
@ -22,9 +24,6 @@ class Place extends Model
|
|||
return 'slug';
|
||||
}
|
||||
|
||||
/** @var array<int, string> */
|
||||
protected $fillable = ['name', 'slug'];
|
||||
|
||||
/** @var array<string, string> */
|
||||
protected $casts = [
|
||||
'latitude' => 'float',
|
||||
|
|
|
|||
|
|
@ -4,40 +4,20 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Visible;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
#[Fillable(['uid', 'name', 'service_name', 'service_url', 'service_photo', 'user_name', 'user_url', 'user_photo'])]
|
||||
#[Visible(['uid', 'name', 'service', 'user'])]
|
||||
#[Appends(['service', 'user'])]
|
||||
class SyndicationTarget extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/** @var array<int, string> */
|
||||
protected $fillable = [
|
||||
'uid',
|
||||
'name',
|
||||
'service_name',
|
||||
'service_url',
|
||||
'service_photo',
|
||||
'user_name',
|
||||
'user_url',
|
||||
'user_photo',
|
||||
];
|
||||
|
||||
/** @var array<int, string> */
|
||||
protected $visible = [
|
||||
'uid',
|
||||
'name',
|
||||
'service',
|
||||
'user',
|
||||
];
|
||||
|
||||
/** @var array<int, string> */
|
||||
protected $appends = [
|
||||
'service',
|
||||
'user',
|
||||
];
|
||||
|
||||
protected function service(): Attribute
|
||||
{
|
||||
return Attribute::get(
|
||||
|
|
|
|||
|
|
@ -4,19 +4,18 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
class Tag extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/** @var array<int, string> */
|
||||
protected $guarded = ['id'];
|
||||
|
||||
public function notes(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Note::class);
|
||||
|
|
|
|||
|
|
@ -4,27 +4,34 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
#[Fillable(['name', 'password'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasFactory;
|
||||
use Notifiable;
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/** @var array<int, string> */
|
||||
protected $fillable = [
|
||||
'name', 'password',
|
||||
];
|
||||
|
||||
/** @var array<int, string> */
|
||||
protected $hidden = [
|
||||
'current_password',
|
||||
'password',
|
||||
'remember_token',
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
public function passkey(): HasMany
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,27 +5,23 @@ declare(strict_types=1);
|
|||
namespace App\Models;
|
||||
|
||||
use App\Traits\FilterHtml;
|
||||
use Codebird\Codebird;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Jonnybarnes\WebmentionsParser\Authorship;
|
||||
|
||||
#[Table('webmentions')]
|
||||
#[Guarded(['id'])]
|
||||
class WebMention extends Model
|
||||
{
|
||||
use FilterHtml;
|
||||
use HasFactory;
|
||||
|
||||
/** @var string */
|
||||
protected $table = 'webmentions';
|
||||
|
||||
/** @var array<int, string> */
|
||||
protected $guarded = ['id'];
|
||||
|
||||
public function commentable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
|
|
@ -127,19 +123,6 @@ class WebMention extends Model
|
|||
return str_replace('http://', 'https://', $url);
|
||||
}
|
||||
|
||||
if ($host === 'twitter.com') {
|
||||
if (Cache::has($url)) {
|
||||
return Cache::get($url);
|
||||
}
|
||||
$username = ltrim(parse_url($url, PHP_URL_PATH), '/');
|
||||
$codebird = resolve(Codebird::class);
|
||||
$info = $codebird->users_show(['screen_name' => $username]);
|
||||
$profile_image = $info->profile_image_url_https;
|
||||
Cache::put($url, $profile_image, 10080); // 1 week
|
||||
|
||||
return $profile_image;
|
||||
}
|
||||
|
||||
$filesystem = new Filesystem;
|
||||
if ($filesystem->exists(public_path().'/assets/profile-images/'.$host.'/image')) {
|
||||
return '/assets/profile-images/'.$host.'/image';
|
||||
|
|
|
|||
|
|
@ -2,14 +2,10 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Note;
|
||||
use App\Observers\NoteObserver;
|
||||
use Codebird\Codebird;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Middleware;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Lcobucci\JWT\Configuration;
|
||||
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||
|
|
@ -33,33 +29,6 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Note::observe(NoteObserver::class);
|
||||
|
||||
// configure Intervention/Image
|
||||
$this->app->bind('Intervention\Image\ImageManager', function () {
|
||||
return \Intervention\Image\ImageManager::withDriver(config('image.driver'));
|
||||
});
|
||||
|
||||
// Bind the Codebird client
|
||||
// Codebird gets mocked in tests
|
||||
// @codeCoverageIgnoreStart
|
||||
$this->app->bind('Codebird\Codebird', function () {
|
||||
Codebird::setConsumerKey(
|
||||
env('TWITTER_CONSUMER_KEY'),
|
||||
env('TWITTER_CONSUMER_SECRET')
|
||||
);
|
||||
|
||||
$cb = Codebird::getInstance();
|
||||
|
||||
$cb->setToken(
|
||||
env('TWITTER_ACCESS_TOKEN'),
|
||||
env('TWITTER_ACCESS_TOKEN_SECRET')
|
||||
);
|
||||
|
||||
return $cb;
|
||||
});
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
/**
|
||||
* Paginate a standard Laravel Collection.
|
||||
*
|
||||
|
|
@ -104,39 +73,10 @@ class AppServiceProvider extends ServiceProvider
|
|||
);
|
||||
});
|
||||
|
||||
// Configure Guzzle
|
||||
$this->app->bind('RetryGuzzle', function () {
|
||||
$handlerStack = \GuzzleHttp\HandlerStack::create();
|
||||
$handlerStack->push(Middleware::retry(
|
||||
function ($retries, $request, $response, $exception) {
|
||||
// Limit the number of retries to 5
|
||||
if ($retries >= 5) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retry connection exceptions
|
||||
if ($exception instanceof \GuzzleHttp\Exception\ConnectException) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Retry on server errors
|
||||
if ($response && $response->getStatusCode() >= 500) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Finally for CloudConvert, retry if status is not final
|
||||
return json_decode($response, false, 512, JSON_THROW_ON_ERROR)->data->status !== 'finished';
|
||||
},
|
||||
function () {
|
||||
// Retry after 1 second
|
||||
return 1000;
|
||||
}
|
||||
));
|
||||
|
||||
return new Client(['handler' => $handlerStack]);
|
||||
});
|
||||
|
||||
// Turn on Eloquent strict mode when developing
|
||||
Model::shouldBeStrict(! $this->app->isProduction());
|
||||
|
||||
// Force HTTPS URL generation in production
|
||||
URL::forceHttps($this->app->isProduction());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\Micropub\CardHandler;
|
||||
use App\Services\Micropub\EntryHandler;
|
||||
use App\Services\Micropub\Handlers\CardHandler;
|
||||
use App\Services\Micropub\Handlers\EntryHandler;
|
||||
use App\Services\Micropub\Handlers\UpdateHandler;
|
||||
use App\Services\Micropub\MicropubHandlerRegistry;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
|
|
@ -19,6 +20,7 @@ class MicropubServiceProvider extends ServiceProvider
|
|||
// Register handlers
|
||||
$registry->register('card', new CardHandler);
|
||||
$registry->register('entry', new EntryHandler);
|
||||
$registry->register('update', new UpdateHandler);
|
||||
|
||||
return $registry;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,10 +8,8 @@ use App\Exceptions\InternetArchiveException;
|
|||
use App\Jobs\ProcessBookmark;
|
||||
use App\Models\Bookmark;
|
||||
use App\Models\Tag;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class BookmarkService
|
||||
|
|
@ -55,21 +53,19 @@ class BookmarkService
|
|||
* Given a URL, attempt to save it to the Internet Archive.
|
||||
*
|
||||
* @throws InternetArchiveException
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function getArchiveLink(string $url): string
|
||||
{
|
||||
$client = resolve(Client::class);
|
||||
try {
|
||||
$response = $client->request('GET', 'https://web.archive.org/save/' . $url);
|
||||
} catch (ClientException $e) {
|
||||
$response = Http::get('https://web.archive.org/save/'.$url);
|
||||
|
||||
if ($response->clientError()) {
|
||||
// throw an exception to be caught
|
||||
throw new InternetArchiveException;
|
||||
}
|
||||
if ($response->hasHeader('Content-Location')) {
|
||||
if (Str::startsWith(Arr::get($response->getHeader('Content-Location'), 0), '/web')) {
|
||||
return $response->getHeader('Content-Location')[0];
|
||||
}
|
||||
|
||||
$contentLocation = $response->header('Content-Location');
|
||||
if ($contentLocation !== '' && Str::startsWith($contentLocation, '/web')) {
|
||||
return $contentLocation;
|
||||
}
|
||||
|
||||
// throw an exception to be caught
|
||||
|
|
|
|||
74
app/Services/Micropub/Data/CardData.php
Normal file
74
app/Services/Micropub/Data/CardData.php
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Micropub\Data;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class CardData extends MicropubData
|
||||
{
|
||||
public function __construct(
|
||||
public readonly array $tokenData,
|
||||
public readonly ?string $name,
|
||||
public readonly ?string $description,
|
||||
public readonly mixed $geo,
|
||||
public readonly mixed $location,
|
||||
public readonly ?string $latitude,
|
||||
public readonly ?string $longitude,
|
||||
) {}
|
||||
|
||||
public static function fromRequest(Request $request): static
|
||||
{
|
||||
if ($request->isJson()) {
|
||||
$data = $request->json()->all();
|
||||
|
||||
return new static(
|
||||
tokenData: $data['token_data'],
|
||||
name: Arr::get($data, 'properties.name.0'),
|
||||
description: Arr::get($data, 'properties.description.0'),
|
||||
geo: Arr::get($data, 'properties.geo.0'),
|
||||
location: Arr::get($data, 'properties.location'),
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
);
|
||||
}
|
||||
|
||||
return new static(
|
||||
tokenData: $request->input('token_data'),
|
||||
name: $request->input('name'),
|
||||
description: $request->input('description'),
|
||||
geo: $request->input('geo'),
|
||||
location: $request->input('location'),
|
||||
latitude: $request->input('latitude'),
|
||||
longitude: $request->input('longitude'),
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new static(
|
||||
tokenData: $data['token_data'],
|
||||
name: $data['name'] ?? null,
|
||||
description: $data['description'] ?? null,
|
||||
geo: $data['geo'] ?? null,
|
||||
location: $data['location'] ?? null,
|
||||
latitude: $data['latitude'] ?? null,
|
||||
longitude: $data['longitude'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'token_data' => $this->tokenData,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'geo' => $this->geo,
|
||||
'location' => $this->location,
|
||||
'latitude' => $this->latitude,
|
||||
'longitude' => $this->longitude,
|
||||
];
|
||||
}
|
||||
}
|
||||
125
app/Services/Micropub/Data/EntryData.php
Normal file
125
app/Services/Micropub/Data/EntryData.php
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Micropub\Data;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class EntryData extends MicropubData
|
||||
{
|
||||
public function __construct(
|
||||
public readonly array $tokenData,
|
||||
public readonly ?string $content,
|
||||
public readonly ?string $inReplyTo,
|
||||
public readonly ?string $published,
|
||||
public readonly mixed $location,
|
||||
public readonly ?string $bookmarkOf,
|
||||
public readonly ?string $likeOf,
|
||||
public readonly ?array $mpSyndicateTo,
|
||||
public readonly ?string $name,
|
||||
public readonly ?string $description,
|
||||
public readonly mixed $geo,
|
||||
public readonly mixed $checkin,
|
||||
public readonly mixed $syndication,
|
||||
public readonly ?array $photos,
|
||||
) {}
|
||||
|
||||
public static function fromRequest(Request $request): static
|
||||
{
|
||||
if ($request->isJson()) {
|
||||
$data = $request->json()->all();
|
||||
|
||||
$rawContent = Arr::get($data, 'properties.content.0');
|
||||
$content = is_array($rawContent) ? ($rawContent['html'] ?? $rawContent['value'] ?? null) : $rawContent;
|
||||
|
||||
return new static(
|
||||
tokenData: $data['token_data'],
|
||||
content: $content,
|
||||
inReplyTo: Arr::get($data, 'properties.in-reply-to.0'),
|
||||
published: Arr::get($data, 'properties.published.0'),
|
||||
location: self::extractLocationData($data),
|
||||
bookmarkOf: Arr::get($data, 'properties.bookmark-of.0'),
|
||||
likeOf: Arr::get($data, 'properties.like-of.0'),
|
||||
mpSyndicateTo: Arr::get($data, 'properties.mp-syndicate-to'),
|
||||
name: Arr::get($data, 'properties.name.0'),
|
||||
description: Arr::get($data, 'properties.description.0'),
|
||||
geo: Arr::get($data, 'properties.geo.0'),
|
||||
checkin: Arr::get($data, 'properties.checkin.0'),
|
||||
syndication: Arr::get($data, 'properties.syndication.0'),
|
||||
photos: Arr::get($data, 'properties.photo'),
|
||||
);
|
||||
}
|
||||
|
||||
return new static(
|
||||
tokenData: $request->input('token_data'),
|
||||
content: $request->input('content'),
|
||||
inReplyTo: $request->input('in-reply-to'),
|
||||
published: $request->input('published'),
|
||||
location: $request->input('location'),
|
||||
bookmarkOf: $request->input('bookmark-of'),
|
||||
likeOf: $request->input('like-of'),
|
||||
mpSyndicateTo: $request->input('mp-syndicate-to'),
|
||||
name: $request->input('name'),
|
||||
description: $request->input('description'),
|
||||
geo: $request->input('geo'),
|
||||
checkin: $request->input('checkin'),
|
||||
syndication: $request->input('syndication'),
|
||||
photos: $request->input('photos'),
|
||||
);
|
||||
}
|
||||
|
||||
private static function extractLocationData(array $data): array|string|null
|
||||
{
|
||||
if (! Arr::has($data, 'properties.location')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Arr::has($data, 'properties.location.0')) {
|
||||
return Arr::get($data, 'properties.location.0');
|
||||
}
|
||||
|
||||
return Arr::get($data, 'properties.location');
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new static(
|
||||
tokenData: $data['token_data'],
|
||||
content: $data['content'] ?? null,
|
||||
inReplyTo: $data['in-reply-to'] ?? null,
|
||||
published: $data['published'] ?? null,
|
||||
location: $data['location'] ?? null,
|
||||
bookmarkOf: $data['bookmark-of'] ?? null,
|
||||
likeOf: $data['like-of'] ?? null,
|
||||
mpSyndicateTo: $data['mp-syndicate-to'] ?? null,
|
||||
name: $data['name'] ?? null,
|
||||
description: $data['description'] ?? null,
|
||||
geo: $data['geo'] ?? null,
|
||||
checkin: $data['checkin'] ?? null,
|
||||
syndication: $data['syndication'] ?? null,
|
||||
photos: $data['photos'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'token_data' => $this->tokenData,
|
||||
'content' => $this->content,
|
||||
'in-reply-to' => $this->inReplyTo,
|
||||
'published' => $this->published,
|
||||
'location' => $this->location,
|
||||
'bookmark-of' => $this->bookmarkOf,
|
||||
'like-of' => $this->likeOf,
|
||||
'mp-syndicate-to' => $this->mpSyndicateTo,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'geo' => $this->geo,
|
||||
'checkin' => $this->checkin,
|
||||
'syndication' => $this->syndication,
|
||||
'photos' => $this->photos,
|
||||
];
|
||||
}
|
||||
}
|
||||
16
app/Services/Micropub/Data/MicropubData.php
Normal file
16
app/Services/Micropub/Data/MicropubData.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Micropub\Data;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
abstract class MicropubData
|
||||
{
|
||||
abstract public static function fromRequest(Request $request): static;
|
||||
|
||||
abstract public static function fromArray(array $data): static;
|
||||
|
||||
abstract public function toArray(): array;
|
||||
}
|
||||
63
app/Services/Micropub/Data/UpdateData.php
Normal file
63
app/Services/Micropub/Data/UpdateData.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Micropub\Data;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UpdateData extends MicropubData
|
||||
{
|
||||
public function __construct(
|
||||
public readonly array $tokenData,
|
||||
public readonly ?string $updateUrl,
|
||||
public readonly ?array $updateReplace,
|
||||
public readonly ?array $updateAdd,
|
||||
public readonly ?array $updateDelete,
|
||||
) {}
|
||||
|
||||
public static function fromRequest(Request $request): static
|
||||
{
|
||||
if ($request->isJson()) {
|
||||
$data = $request->json()->all();
|
||||
|
||||
return new static(
|
||||
tokenData: $data['token_data'],
|
||||
updateUrl: $data['url'] ?? null,
|
||||
updateReplace: $data['replace'] ?? null,
|
||||
updateAdd: $data['add'] ?? null,
|
||||
updateDelete: $data['delete'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
return new static(
|
||||
tokenData: $request->input('token_data'),
|
||||
updateUrl: $request->input('url'),
|
||||
updateReplace: $request->input('replace'),
|
||||
updateAdd: $request->input('add'),
|
||||
updateDelete: $request->input('delete'),
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new static(
|
||||
tokenData: $data['token_data'],
|
||||
updateUrl: $data['update_url'] ?? null,
|
||||
updateReplace: $data['update_replace'] ?? null,
|
||||
updateAdd: $data['update_add'] ?? null,
|
||||
updateDelete: $data['update_delete'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'token_data' => $this->tokenData,
|
||||
'update_url' => $this->updateUrl,
|
||||
'update_replace' => $this->updateReplace,
|
||||
'update_add' => $this->updateAdd,
|
||||
'update_delete' => $this->updateDelete,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -2,20 +2,28 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Micropub;
|
||||
namespace App\Services\Micropub\Handlers;
|
||||
|
||||
use App\Exceptions\InvalidTokenScopeException;
|
||||
use App\Services\Micropub\Data\CardData;
|
||||
use App\Services\Micropub\Data\MicropubData;
|
||||
use App\Services\PlaceService;
|
||||
|
||||
class CardHandler implements MicropubHandlerInterface
|
||||
{
|
||||
public function dataClass(): string
|
||||
{
|
||||
return CardData::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidTokenScopeException
|
||||
*/
|
||||
public function handle(array $data): array
|
||||
public function handle(MicropubData $data): array
|
||||
{
|
||||
// Handle h-card requests
|
||||
$scopes = $data['token_data']['scope'];
|
||||
assert($data instanceof CardData);
|
||||
|
||||
$scopes = $data->tokenData['scope'];
|
||||
if (is_string($scopes)) {
|
||||
$scopes = explode(' ', $scopes);
|
||||
}
|
||||
|
|
@ -24,7 +32,7 @@ class CardHandler implements MicropubHandlerInterface
|
|||
throw new InvalidTokenScopeException;
|
||||
}
|
||||
|
||||
$location = resolve(PlaceService::class)->createPlace($data)->uri;
|
||||
$location = resolve(PlaceService::class)->createPlace($data->toArray())->uri;
|
||||
|
||||
return [
|
||||
'response' => 'created',
|
||||
|
|
@ -2,22 +2,31 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Micropub;
|
||||
namespace App\Services\Micropub\Handlers;
|
||||
|
||||
use App\Exceptions\InvalidTokenScopeException;
|
||||
use App\Services\ArticleService;
|
||||
use App\Services\BookmarkService;
|
||||
use App\Services\LikeService;
|
||||
use App\Services\Micropub\Data\EntryData;
|
||||
use App\Services\Micropub\Data\MicropubData;
|
||||
use App\Services\NoteService;
|
||||
|
||||
class EntryHandler implements MicropubHandlerInterface
|
||||
{
|
||||
public function dataClass(): string
|
||||
{
|
||||
return EntryData::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidTokenScopeException
|
||||
*/
|
||||
public function handle(array $data)
|
||||
public function handle(MicropubData $data): array
|
||||
{
|
||||
$scopes = $data['token_data']['scope'];
|
||||
assert($data instanceof EntryData);
|
||||
|
||||
$scopes = $data->tokenData['scope'];
|
||||
if (is_string($scopes)) {
|
||||
$scopes = explode(' ', $scopes);
|
||||
}
|
||||
|
|
@ -26,11 +35,12 @@ class EntryHandler implements MicropubHandlerInterface
|
|||
throw new InvalidTokenScopeException;
|
||||
}
|
||||
|
||||
$dataArray = $data->toArray();
|
||||
$location = match (true) {
|
||||
isset($data['like-of']) => resolve(LikeService::class)->create($data)->url,
|
||||
isset($data['bookmark-of']) => resolve(BookmarkService::class)->create($data)->uri,
|
||||
isset($data['name']) => resolve(ArticleService::class)->create($data)->link,
|
||||
default => resolve(NoteService::class)->create($data)->uri,
|
||||
isset($dataArray['like-of']) => resolve(LikeService::class)->create($dataArray)->url,
|
||||
isset($dataArray['bookmark-of']) => resolve(BookmarkService::class)->create($dataArray)->uri,
|
||||
isset($dataArray['name']) => resolve(ArticleService::class)->create($dataArray)->link,
|
||||
default => resolve(NoteService::class)->create($dataArray)->uri,
|
||||
};
|
||||
|
||||
return [
|
||||
44
app/Services/Micropub/Handlers/MicropubHandlerInterface.php
Normal file
44
app/Services/Micropub/Handlers/MicropubHandlerInterface.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Micropub\Handlers;
|
||||
|
||||
use App\Services\Micropub\Data\MicropubData;
|
||||
|
||||
/**
|
||||
* Contract for all Micropub request handlers.
|
||||
*
|
||||
* The Micropub endpoint receives different types of request — creating an
|
||||
* h-entry (note, article, bookmark, like), creating an h-card (place), or
|
||||
* updating an existing post. Each type is handled by its own class that
|
||||
* implements this interface.
|
||||
*
|
||||
* Each handler declares the typed data object it needs via dataClass(). The
|
||||
* controller calls dataClass()::fromArray() to build the right object before
|
||||
* passing it to handle(), so the handler always receives a concrete typed
|
||||
* object rather than a raw array.
|
||||
*
|
||||
* Handlers live in App\Services\Micropub\Handlers.
|
||||
* Their corresponding data objects live in App\Services\Micropub\Data.
|
||||
* Handlers are registered in MicropubServiceProvider and looked up by type
|
||||
* string (e.g. "entry", "card", "update") via MicropubHandlerRegistry.
|
||||
*/
|
||||
interface MicropubHandlerInterface
|
||||
{
|
||||
/**
|
||||
* Return the fully-qualified class name of the data object this handler
|
||||
* expects. The controller will call fromArray() on this class to build the
|
||||
* object from the normalised request data before calling handle().
|
||||
*
|
||||
* @return class-string<MicropubData>
|
||||
*/
|
||||
public function dataClass(): string;
|
||||
|
||||
/**
|
||||
* Process the request and return a result array with at minimum a
|
||||
* 'response' key ('created' or 'updated') and a 'url' key pointing to
|
||||
* the affected resource.
|
||||
*/
|
||||
public function handle(MicropubData $data): array;
|
||||
}
|
||||
150
app/Services/Micropub/Handlers/UpdateHandler.php
Normal file
150
app/Services/Micropub/Handlers/UpdateHandler.php
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Micropub\Handlers;
|
||||
|
||||
use App\Exceptions\InvalidTokenScopeException;
|
||||
use App\Exceptions\MicropubHandlerException;
|
||||
use App\Exceptions\MicropubUnsupportedModelException;
|
||||
use App\Models\Media;
|
||||
use App\Models\Note;
|
||||
use App\Services\Micropub\Data\MicropubData;
|
||||
use App\Services\Micropub\Data\UpdateData;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UpdateHandler implements MicropubHandlerInterface
|
||||
{
|
||||
public function dataClass(): string
|
||||
{
|
||||
return UpdateData::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidTokenScopeException
|
||||
* @throws MicropubUnsupportedModelException
|
||||
* @throws MicropubHandlerException
|
||||
*/
|
||||
public function handle(MicropubData $data): array
|
||||
{
|
||||
assert($data instanceof UpdateData);
|
||||
|
||||
$scopes = $data->tokenData['scope'];
|
||||
if (is_string($scopes)) {
|
||||
$scopes = explode(' ', $scopes);
|
||||
}
|
||||
|
||||
if (! in_array('update', $scopes, true)) {
|
||||
throw new InvalidTokenScopeException;
|
||||
}
|
||||
|
||||
$urlPath = parse_url($data->updateUrl, PHP_URL_PATH);
|
||||
|
||||
if (mb_substr($urlPath, 1, 5) !== 'notes') {
|
||||
throw new MicropubUnsupportedModelException('This implementation currently only supports the updating of notes');
|
||||
}
|
||||
|
||||
$note = Note::nb60(basename($urlPath))->firstOrFail();
|
||||
|
||||
if ($data->updateReplace === null && $data->updateAdd === null && $data->updateDelete === null) {
|
||||
throw new MicropubHandlerException('Unsupported update operation');
|
||||
}
|
||||
|
||||
if ($data->updateReplace !== null) {
|
||||
foreach ($data->updateReplace as $property => $value) {
|
||||
match ($property) {
|
||||
'content' => $note->note = is_array($value[0]) ? ($value[0]['html'] ?? $value[0]['value'] ?? null) : $value[0],
|
||||
'syndication' => $this->applySyndication($note, $value),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if ($data->updateAdd !== null) {
|
||||
foreach ($data->updateAdd as $property => $value) {
|
||||
if ($property === 'syndication') {
|
||||
$this->applySyndication($note, $value);
|
||||
}
|
||||
if ($property === 'photo') {
|
||||
foreach ($value as $photoURL) {
|
||||
if (Str::startsWith($photoURL, 'https://')) {
|
||||
$media = new Media;
|
||||
$media->path = $photoURL;
|
||||
$media->type = 'image';
|
||||
$media->save();
|
||||
$note->media()->save($media);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($data->updateDelete !== null) {
|
||||
$this->applyDelete($note, $data->updateDelete);
|
||||
}
|
||||
|
||||
$note->save();
|
||||
|
||||
return [
|
||||
'response' => 'updated',
|
||||
'url' => $note->uri,
|
||||
];
|
||||
}
|
||||
|
||||
private function applyDelete(Note $note, array $delete): void
|
||||
{
|
||||
if (array_is_list($delete)) {
|
||||
foreach ($delete as $property) {
|
||||
match ($property) {
|
||||
'syndication' => $this->clearSyndication($note),
|
||||
'photo' => $note->media()->delete(),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($delete as $property => $values) {
|
||||
if ($property === 'syndication') {
|
||||
$this->removeSyndicationValues($note, $values);
|
||||
}
|
||||
if ($property === 'photo') {
|
||||
$note->media()->whereIn('path', $values)->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function clearSyndication(Note $note): void
|
||||
{
|
||||
$note->facebook_url = null;
|
||||
$note->swarm_url = null;
|
||||
$note->tweet_id = null;
|
||||
}
|
||||
|
||||
private function removeSyndicationValues(Note $note, array $urls): void
|
||||
{
|
||||
foreach ($urls as $url) {
|
||||
if (Str::startsWith($url, 'https://www.facebook.com') && $note->facebook_url === $url) {
|
||||
$note->facebook_url = null;
|
||||
} elseif (Str::startsWith($url, 'https://www.swarmapp.com') && $note->swarm_url === $url) {
|
||||
$note->swarm_url = null;
|
||||
} elseif (Str::startsWith($url, 'https://twitter.com')) {
|
||||
$note->tweet_id = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function applySyndication(Note $note, array $urls): void
|
||||
{
|
||||
foreach ($urls as $url) {
|
||||
if (Str::startsWith($url, 'https://www.facebook.com')) {
|
||||
$note->facebook_url = $url;
|
||||
} elseif (Str::startsWith($url, 'https://www.swarmapp.com')) {
|
||||
$note->swarm_url = $url;
|
||||
} elseif (Str::startsWith($url, 'https://twitter.com')) {
|
||||
$note->tweet_id = basename(parse_url($url, PHP_URL_PATH));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Micropub;
|
||||
|
||||
interface MicropubHandlerInterface
|
||||
{
|
||||
public function handle(array $data);
|
||||
}
|
||||
|
|
@ -5,7 +5,27 @@ declare(strict_types=1);
|
|||
namespace App\Services\Micropub;
|
||||
|
||||
use App\Exceptions\MicropubHandlerException;
|
||||
use App\Services\Micropub\Handlers\MicropubHandlerInterface;
|
||||
|
||||
/**
|
||||
* Maps Micropub post types to their handler instances.
|
||||
*
|
||||
* MicropubRequest normalises every incoming request and resolves it to a type
|
||||
* string ("entry", "card", "update"). The controller asks the registry for the
|
||||
* right handler, then asks the handler which data class to build, and finally
|
||||
* calls handle() with that data object.
|
||||
*
|
||||
* Flow:
|
||||
* MicropubRequest (normalise) → MicropubController
|
||||
* → MicropubHandlerRegistry::getHandler($type)
|
||||
* → $handler->dataClass()::fromArray($rawData)
|
||||
* → $handler->handle($dataObject)
|
||||
*
|
||||
* Handlers are registered in MicropubServiceProvider. To support a new
|
||||
* Micropub post type, create a handler in App\Services\Micropub\Handlers, a
|
||||
* matching data class in App\Services\Micropub\Data, and register the handler
|
||||
* here with its type string.
|
||||
*/
|
||||
class MicropubHandlerRegistry
|
||||
{
|
||||
/**
|
||||
|
|
@ -13,6 +33,9 @@ class MicropubHandlerRegistry
|
|||
*/
|
||||
protected array $handlers = [];
|
||||
|
||||
/**
|
||||
* Register a handler for a given Micropub type string.
|
||||
*/
|
||||
public function register(string $type, MicropubHandlerInterface $handler): self
|
||||
{
|
||||
$this->handlers[$type] = $handler;
|
||||
|
|
@ -21,6 +44,8 @@ class MicropubHandlerRegistry
|
|||
}
|
||||
|
||||
/**
|
||||
* Retrieve the handler for a given type, or throw if none is registered.
|
||||
*
|
||||
* @throws MicropubHandlerException
|
||||
*/
|
||||
public function getHandler(string $type): MicropubHandlerInterface
|
||||
|
|
|
|||
|
|
@ -1,119 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Micropub;
|
||||
|
||||
use App\Exceptions\InvalidTokenScopeException;
|
||||
use App\Models\Media;
|
||||
use App\Models\Note;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/*
|
||||
* @todo Implement this properly
|
||||
*/
|
||||
class UpdateHandler implements MicropubHandlerInterface
|
||||
{
|
||||
/**
|
||||
* @throws InvalidTokenScopeException
|
||||
*/
|
||||
public function handle(array $data)
|
||||
{
|
||||
$scopes = $data['token_data']['scope'];
|
||||
if (is_string($scopes)) {
|
||||
$scopes = explode(' ', $scopes);
|
||||
}
|
||||
|
||||
if (! in_array('update', $scopes, true)) {
|
||||
throw new InvalidTokenScopeException;
|
||||
}
|
||||
|
||||
$urlPath = parse_url(Arr::get($data, 'url'), PHP_URL_PATH);
|
||||
|
||||
// is it a note we are updating?
|
||||
if (mb_substr($urlPath, 1, 5) !== 'notes') {
|
||||
return response()->json([
|
||||
'error' => 'invalid',
|
||||
'error_description' => 'This implementation currently only support the updating of notes',
|
||||
], 500);
|
||||
}
|
||||
|
||||
try {
|
||||
$note = Note::nb60(basename($urlPath))->firstOrFail();
|
||||
} catch (ModelNotFoundException) {
|
||||
return response()->json([
|
||||
'error' => 'invalid_request',
|
||||
'error_description' => 'No known note with given ID',
|
||||
], 404);
|
||||
}
|
||||
|
||||
// got the note, are we dealing with a “replace” request?
|
||||
if (Arr::get($data, 'replace')) {
|
||||
foreach (Arr::get($data, 'replace') as $property => $value) {
|
||||
if ($property === 'content') {
|
||||
$note->note = $value[0];
|
||||
}
|
||||
if ($property === 'syndication') {
|
||||
foreach ($value as $syndicationURL) {
|
||||
if (Str::startsWith($syndicationURL, 'https://www.facebook.com')) {
|
||||
$note->facebook_url = $syndicationURL;
|
||||
}
|
||||
if (Str::startsWith($syndicationURL, 'https://www.swarmapp.com')) {
|
||||
$note->swarm_url = $syndicationURL;
|
||||
}
|
||||
if (Str::startsWith($syndicationURL, 'https://twitter.com')) {
|
||||
$note->tweet_id = basename(parse_url($syndicationURL, PHP_URL_PATH));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$note->save();
|
||||
|
||||
return [
|
||||
'response' => 'updated',
|
||||
];
|
||||
}
|
||||
|
||||
// how about “add”
|
||||
if (Arr::get($data, 'add')) {
|
||||
foreach (Arr::get($data, 'add') as $property => $value) {
|
||||
if ($property === 'syndication') {
|
||||
foreach ($value as $syndicationURL) {
|
||||
if (Str::startsWith($syndicationURL, 'https://www.facebook.com')) {
|
||||
$note->facebook_url = $syndicationURL;
|
||||
}
|
||||
if (Str::startsWith($syndicationURL, 'https://www.swarmapp.com')) {
|
||||
$note->swarm_url = $syndicationURL;
|
||||
}
|
||||
if (Str::startsWith($syndicationURL, 'https://twitter.com')) {
|
||||
$note->tweet_id = basename(parse_url($syndicationURL, PHP_URL_PATH));
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($property === 'photo') {
|
||||
foreach ($value as $photoURL) {
|
||||
if (Str::startsWith($photoURL, 'https://')) {
|
||||
$media = new Media;
|
||||
$media->path = $photoURL;
|
||||
$media->type = 'image';
|
||||
$media->save();
|
||||
$note->media()->save($media);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$note->save();
|
||||
|
||||
return response()->json([
|
||||
'response' => 'updated',
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error_description' => 'unsupported request',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
|
@ -48,12 +48,15 @@ class NoteService
|
|||
$note->place()->associate($this->getCheckin($data));
|
||||
$note->swarm_url = $this->getSwarmUrl($data);
|
||||
}
|
||||
//
|
||||
|
||||
// $note->instagram_url = $this->getInstagramUrl($request);
|
||||
//
|
||||
// foreach ($this->getMedia($request) as $media) {
|
||||
// $note->media()->save($media);
|
||||
// }
|
||||
|
||||
foreach ($this->getMedia($data) as $index => $media) {
|
||||
$note->media()->attach($media['value']->id, [
|
||||
'alt_text' => $media['alt'],
|
||||
'order' => $index,
|
||||
]);
|
||||
}
|
||||
|
||||
$note->save();
|
||||
|
||||
|
|
@ -188,23 +191,44 @@ class NoteService
|
|||
/**
|
||||
* Get the media URLs from the request to create a new note.
|
||||
*/
|
||||
private function getMedia(array $request): array
|
||||
private function getMedia(array $data): array
|
||||
{
|
||||
$media = [];
|
||||
$photos = Arr::get($request, 'photo') ?? Arr::get($request, 'properties.photo');
|
||||
$photos = Arr::get($data, 'photos');
|
||||
|
||||
if (isset($photos)) {
|
||||
foreach ((array) $photos as $photo) {
|
||||
// $photo can be a string of the URL opf the photo
|
||||
// or it can be an object with a `value` and `alt`
|
||||
$photoUrl = null;
|
||||
$photoAlt = null;
|
||||
if (is_string($photo)) {
|
||||
$photoUrl = $photo;
|
||||
} elseif (is_array($photo)) {
|
||||
$photoUrl = $photo['value'];
|
||||
$photoAlt = $photo['alt'];
|
||||
}
|
||||
|
||||
if (empty($photoUrl)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check the media was uploaded to my endpoint, and use path
|
||||
if (Str::startsWith($photo, config('filesystems.disks.s3.url'))) {
|
||||
$path = substr($photo, strlen(config('filesystems.disks.s3.url')));
|
||||
$media[] = Media::where('path', ltrim($path, '/'))->firstOrFail();
|
||||
if (Str::startsWith($photoUrl, config('filesystems.disks.public.url'))) {
|
||||
$path = substr($photoUrl, strlen(config('filesystems.disks.public.url')));
|
||||
$media[] = [
|
||||
'value' => Media::where('path', ltrim($path, '/'))->firstOrFail(),
|
||||
'alt' => $photoAlt,
|
||||
];
|
||||
} else {
|
||||
$newMedia = Media::firstOrNew(['path' => $photo]);
|
||||
$newMedia = Media::firstOrNew(['path' => $photoUrl]);
|
||||
// currently assuming this is a photo from Swarm or OwnYourGram
|
||||
$newMedia->type = 'image';
|
||||
$newMedia->save();
|
||||
$media[] = $newMedia;
|
||||
$media[] = [
|
||||
'value' => $newMedia,
|
||||
'alt' => $photoAlt,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,14 +15,17 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||
->withMiddleware(function (Middleware $middleware) {
|
||||
$middleware
|
||||
->append(LinkHeadersMiddleware::class)
|
||||
->validateCsrfTokens(except: [
|
||||
->preventRequestForgery(
|
||||
except: [
|
||||
'auth', // This is the IndieAuth auth endpoint
|
||||
'token', // This is the IndieAuth token endpoint
|
||||
'api/post',
|
||||
'api/media',
|
||||
'micropub/places',
|
||||
'webmention',
|
||||
]);
|
||||
],
|
||||
originOnly: true
|
||||
);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
Flare::handles($exceptions);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
<?php
|
||||
|
||||
use App\Providers\AppServiceProvider;
|
||||
use App\Providers\HorizonServiceProvider;
|
||||
use App\Providers\MicropubServiceProvider;
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\HorizonServiceProvider::class,
|
||||
App\Providers\MicropubServiceProvider::class,
|
||||
AppServiceProvider::class,
|
||||
HorizonServiceProvider::class,
|
||||
MicropubServiceProvider::class,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -6,49 +6,43 @@
|
|||
"keywords": ["laravel", "framework", "indieweb"],
|
||||
"license": "CC0-1.0",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"php": "^8.3",
|
||||
"ext-dom": "*",
|
||||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-pgsql": "*",
|
||||
"ext-sodium": "*",
|
||||
"cviebrock/eloquent-sluggable": "^12.0",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"cviebrock/eloquent-sluggable": "^13.0",
|
||||
"indieauth/client": "^1.1",
|
||||
"intervention/image": "^3",
|
||||
"intervention/image": "^4.0",
|
||||
"jonnybarnes/indieweb": "~0.2",
|
||||
"jonnybarnes/webmentions-parser": "~0.5",
|
||||
"jublonet/codebird-php": "4.0.0-beta.1",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/horizon": "^5.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/scout": "^10.1",
|
||||
"laravel/tinker": "^2.8",
|
||||
"laravel/tinker": "^3.0",
|
||||
"lcobucci/jwt": "^5.0",
|
||||
"league/commonmark": "^2.0",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"mf2/mf2": "~0.3",
|
||||
"phpdocumentor/reflection-docblock": "^5.3",
|
||||
"spatie/commonmark-highlighter": "^3.0",
|
||||
"spatie/laravel-flare": "^2.2",
|
||||
"symfony/html-sanitizer": "^7.0",
|
||||
"symfony/property-access": "^7.0",
|
||||
"symfony/serializer": "^7.0",
|
||||
"spatie/laravel-flare": "^3.0",
|
||||
"symfony/html-sanitizer": "^8.0",
|
||||
"tempest/highlight": "^2.27",
|
||||
"web-auth/webauthn-lib": "^5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.0",
|
||||
"barryvdh/laravel-debugbar": "^4.0.9",
|
||||
"barryvdh/laravel-ide-helper": "^3.0",
|
||||
"fakerphp/faker": "^1.9.2",
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/dusk": "^8.0",
|
||||
"laravel/pail": "^1.2",
|
||||
"laravel/pint": "^1.0",
|
||||
"laravel/sail": "^1.18",
|
||||
"mockery/mockery": "^1.4.4",
|
||||
"nunomaduro/collision": "^8.1",
|
||||
"openai-php/client": "^0.17.1",
|
||||
"phpunit/php-code-coverage": "^11.0",
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"laravel/pail": "^1.2.5",
|
||||
"laravel/pint": "^1.27",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/php-code-coverage": "^14.0",
|
||||
"phpunit/phpunit": "^13.0",
|
||||
"spatie/laravel-ray": "^1.12",
|
||||
"spatie/x-ray": "^1.2"
|
||||
},
|
||||
|
|
@ -68,6 +62,22 @@
|
|||
}
|
||||
},
|
||||
"scripts": {
|
||||
"setup": [
|
||||
"composer install",
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||
"@php artisan key:generate",
|
||||
"@php artisan migrate --force",
|
||||
"npm install --ignore-scripts",
|
||||
"npm run build"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
|
|
@ -83,9 +93,8 @@
|
|||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
|
||||
"pre-package-uninstall": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
|
|
@ -99,8 +108,7 @@
|
|||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true,
|
||||
"composer/installers": true
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
|
|
|
|||
4349
composer.lock
generated
4349
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -101,7 +101,7 @@ return [
|
|||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', env('APP_PREVIOUS_KEYS', ''))
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|
|
@ -62,7 +64,7 @@ return [
|
|||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
'model' => env('AUTH_MODEL', User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
|
|
@ -104,7 +106,7 @@ return [
|
|||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the amount of seconds before a password confirmation
|
||||
| Here you may define the number of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mastodon Token
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When syndicating posts to Mastodon using Brid.gy’s Micropub endpoint, we
|
||||
| need to provide an access token. This token can be generated by going to
|
||||
| https://brid.gy/mastodon and clicking the “Get token” button.
|
||||
|
|
||||
*/
|
||||
|
||||
'mastodon_token' => env('BRIDGY_MASTODON_TOKEN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Bluesky Token
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When syndicating posts to Bluesky using Brid.gy’s Micropub endpoint, we
|
||||
| need to provide an access token. This token can be generated by going to
|
||||
| https://brid.gy/bluesky and clicking the “Get token” button.
|
||||
|
|
||||
*/
|
||||
|
||||
'bluesky_token' => env('BRIDGY_BLUESKY_TOKEN'),
|
||||
|
||||
];
|
||||
|
|
@ -26,8 +26,9 @@ return [
|
|||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "apc", "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "octane", "null"
|
||||
| Supported drivers: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "octane",
|
||||
| "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
|
|
@ -40,9 +41,10 @@ return [
|
|||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
|
|
@ -89,6 +91,14 @@ return [
|
|||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'stores' => [
|
||||
'database',
|
||||
'array',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
@ -102,6 +112,19 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Serializable Classes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the classes that can be unserialized from cache
|
||||
| storage. By default, no PHP classes will be unserialized from your
|
||||
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
||||
|
|
||||
*/
|
||||
|
||||
'serializable_classes' => false,
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Pdo\Mysql;
|
||||
|
||||
return [
|
||||
|
||||
|
|
@ -40,6 +41,7 @@ return [
|
|||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
|
|
@ -58,7 +60,7 @@ return [
|
|||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
|
|
@ -78,7 +80,7 @@ return [
|
|||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
|
|
@ -94,7 +96,7 @@ return [
|
|||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
|
|
@ -147,7 +149,7 @@ return [
|
|||
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
|
|
@ -158,6 +160,10 @@ return [
|
|||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
|
|
@ -167,6 +173,10 @@ return [
|
|||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
],
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ return [
|
|||
| may even configure multiple disks for the same driver. Examples for
|
||||
| most supported storage drivers are configured here for reference.
|
||||
|
|
||||
| Supported Drivers: "local", "ftp", "sftp", "s3"
|
||||
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||
|
|
||||
*/
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ return [
|
|||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL').'/storage',
|
||||
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
|
|
|
|||
129
config/flare.php
129
config/flare.php
|
|
@ -1,8 +1,8 @@
|
|||
<?php
|
||||
|
||||
use Spatie\FlareClient\Api;
|
||||
use Spatie\LaravelFlare\AttributesProviders\LaravelUserAttributesProvider;
|
||||
use Spatie\FlareClient\Sampling\RateSampler;
|
||||
use Spatie\LaravelFlare\FlareConfig;
|
||||
use Spatie\LaravelFlare\Senders\LaravelHttpSender;
|
||||
|
||||
return [
|
||||
/*
|
||||
|
|
@ -19,17 +19,6 @@ return [
|
|||
|
||||
'key' => env('FLARE_KEY'),
|
||||
|
||||
/*
|
||||
|
|
||||
|--------------------------------------------------------------------------
|
||||
| Flare Base URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Which server should be used to send the reports/traces to.
|
||||
|
|
||||
*/
|
||||
'base_url' => env('FLARE_BASE_URL', Api::BASE_URL),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Collects
|
||||
|
|
@ -45,22 +34,6 @@ return [
|
|||
extra: []
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Attribute providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When sending an error report or trace to Flare attributes can be added to
|
||||
| the report or trace for common entries. An example of such an entry is
|
||||
| the currently authenticated user. In an attribute provider you can
|
||||
| specify which attributes should be sent.
|
||||
|
|
||||
*/
|
||||
|
||||
'attribute_providers' => [
|
||||
'user' => LaravelUserAttributesProvider::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Censor data
|
||||
|
|
@ -86,19 +59,49 @@ return [
|
|||
'X-XSRF-TOKEN',
|
||||
],
|
||||
'client_ips' => false,
|
||||
'cookies' => false,
|
||||
'session' => false,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reporting log statements
|
||||
| Sender
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If this setting is `false` log statements won't be sent as events to Flare,
|
||||
| no matter which error level you specified in the Flare log channel.
|
||||
| The sender is responsible for sending the error reports and traces to
|
||||
| Flare. By default, Laravel Flare sends them over HTTP. To use the local
|
||||
| Flare daemon, switch the sender class to
|
||||
| `Spatie\FlareClient\Senders\DaemonSender::class` and set `daemon_url`.
|
||||
| The daemon sender defaults to localhost on port 8787 and uses its own
|
||||
| default timeouts and fallback sender config unless you override them.
|
||||
|
|
||||
*/
|
||||
|
||||
'send_logs_as_events' => true,
|
||||
'sender' => [
|
||||
'class' => LaravelHttpSender::class,
|
||||
'config' => [
|
||||
'timeout' => 10,
|
||||
],
|
||||
],
|
||||
|
||||
// Daemon sender example
|
||||
// 'sender' => [
|
||||
// 'class' => \Spatie\FlareClient\Senders\DaemonSender::class,
|
||||
// 'config' => [
|
||||
// 'daemon_url' => env('FLARE_DAEMON_URL', 'http://127.0.0.1:8787'),
|
||||
// ],
|
||||
// ],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Report
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Flare reports errors and exceptions happening within your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'report' => env('FLARE_REPORT', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
@ -111,19 +114,6 @@ return [
|
|||
|
||||
'report_error_levels' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Share button
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Flare automatically adds a Share button to the laravel error page. This
|
||||
| button allows you to easily share errors with colleagues or friends. It
|
||||
| is enabled by default, but you can disable it here.
|
||||
|
|
||||
*/
|
||||
|
||||
'enable_share_button' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Override grouping
|
||||
|
|
@ -143,20 +133,16 @@ return [
|
|||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sender
|
||||
| Share button
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The sender is responsible for sending the error reports and traces to
|
||||
| Flare it can be configured if needed.
|
||||
| Flare automatically adds a Share button to the laravel error page. This
|
||||
| button allows you to easily share errors with colleagues or friends. It
|
||||
| is enabled by default, but you can disable it here.
|
||||
|
|
||||
*/
|
||||
|
||||
'sender' => [
|
||||
'class' => \Spatie\LaravelFlare\Senders\LaravelHttpSender::class,
|
||||
'config' => [
|
||||
'timeout' => 10,
|
||||
],
|
||||
],
|
||||
'enable_share_button' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
@ -168,7 +154,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'trace' => env('FLARE_TRACE', false),
|
||||
'trace' => env('FLARE_TRACE', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
@ -181,26 +167,35 @@ return [
|
|||
| which means that 10% of the traces will be recorded.
|
||||
|
|
||||
*/
|
||||
|
||||
'sampler' => [
|
||||
'class' => \Spatie\FlareClient\Sampling\RateSampler::class,
|
||||
'class' => RateSampler::class,
|
||||
'config' => [
|
||||
'rate' => 0.1,
|
||||
'rate' => env('FLARE_SAMPLER_RATE', 0.1),
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Trace limits
|
||||
| Log
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Limits for the tracing data. These limits are used to prevent
|
||||
| the tracing data from growing too large.
|
||||
| Logging show you an overview of log entries within your application.
|
||||
|
|
||||
*/
|
||||
'trace_limits' => [
|
||||
'max_spans' => 512,
|
||||
'max_attributes_per_span' => 128,
|
||||
'max_span_events_per_span' => 128,
|
||||
'max_attributes_per_span_event' => 128,
|
||||
],
|
||||
|
||||
'log' => env('FLARE_LOG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Minimal log level
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You can specify the minimal (Monolog) log level that should be sent to Flare.
|
||||
| Log levels lower than the specified level will be ignored.
|
||||
| If null all log levels will be sent to Flare.
|
||||
|
|
||||
*/
|
||||
|
||||
'minimal_log_level' => null,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Intervention\Image\Drivers\Gd\Driver;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Image Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Intervention Image supports "GD Library" and "Imagick" to process images
|
||||
| internally. You may choose one of them according to your PHP
|
||||
| configuration. By default PHP's "GD Library" implementation is used.
|
||||
|
|
||||
| Supported: "gd", "imagick"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => Driver::class,
|
||||
|
||||
];
|
||||
|
|
@ -45,7 +45,7 @@ return [
|
|||
| utilizes the Monolog PHP logging library, which includes a variety
|
||||
| of powerful log handlers and formatters that you're free to use.
|
||||
|
|
||||
| Available Drivers: "single", "daily", "slack", "syslog",
|
||||
| Available drivers: "single", "daily", "slack", "syslog",
|
||||
| "errorlog", "monolog", "custom", "stack"
|
||||
|
|
||||
*/
|
||||
|
|
@ -54,7 +54,7 @@ return [
|
|||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', env('LOG_STACK', 'single')),
|
||||
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ return [
|
|||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
|
||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||
'level' => env('LOG_LEVEL', 'critical'),
|
||||
'replace_placeholders' => true,
|
||||
|
|
@ -98,10 +98,10 @@ return [
|
|||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'with' => [
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ return [
|
|||
| your mailers below. You may also add additional mailers if needed.
|
||||
|
|
||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||
| "postmark", "log", "array", "failover", "roundrobin"
|
||||
| "postmark", "resend", "log", "array",
|
||||
| "failover", "roundrobin"
|
||||
|
|
||||
*/
|
||||
|
||||
|
|
@ -45,7 +46,7 @@ return [
|
|||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
|
|
@ -60,6 +61,10 @@ return [
|
|||
// ],
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'transport' => 'resend',
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||
|
|
@ -80,6 +85,16 @@ return [
|
|||
'smtp',
|
||||
'log',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
],
|
||||
|
|
@ -97,7 +112,7 @@ return [
|
|||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ return [
|
|||
| used by your application. An example configuration is provided for
|
||||
| each backend supported by Laravel. You're also free to add more.
|
||||
|
|
||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
|
||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||
| "deferred", "background", "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
|
|
@ -72,6 +73,22 @@ return [
|
|||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'deferred' => [
|
||||
'driver' => 'deferred',
|
||||
],
|
||||
|
||||
'background' => [
|
||||
'driver' => 'background',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'connections' => [
|
||||
'database',
|
||||
'deferred',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Stateful Domains
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Requests from the following domains / hosts will receive stateful API
|
||||
| authentication cookies. Typically, these should include your local
|
||||
| and production domains which access your API via a frontend SPA.
|
||||
|
|
||||
*/
|
||||
|
||||
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||
'%s%s',
|
||||
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||
Sanctum::currentApplicationUrlWithPort()
|
||||
))),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sanctum Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This array contains the authentication guards that will be checked when
|
||||
| Sanctum is trying to authenticate a request. If none of these guards
|
||||
| are able to authenticate the request, Sanctum will use the bearer
|
||||
| token that's present on an incoming request for authentication.
|
||||
|
|
||||
*/
|
||||
|
||||
'guard' => ['web'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expiration Minutes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value controls the number of minutes until an issued token will be
|
||||
| considered expired. If this value is null, personal access tokens do
|
||||
| not expire. This won't tweak the lifetime of first-party sessions.
|
||||
|
|
||||
*/
|
||||
|
||||
'expiration' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sanctum Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When authenticating your first-party SPA with Sanctum you may need to
|
||||
| customize some of the middleware Sanctum uses while processing the
|
||||
| request. You may change the middleware listed below as required.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => [
|
||||
'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
|
||||
'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -15,7 +15,11 @@ return [
|
|||
*/
|
||||
|
||||
'postmark' => [
|
||||
'token' => env('POSTMARK_TOKEN'),
|
||||
'key' => env('POSTMARK_API_KEY'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_API_KEY'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
|
|
@ -35,4 +39,8 @@ return [
|
|||
'token' => env('CLOUDCONVERT_API_TOKEN'),
|
||||
],
|
||||
|
||||
'brrr' => [
|
||||
'webhook_url' => env('BRRR_WEBHOOK_URL'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ return [
|
|||
| incoming requests. Laravel supports a variety of storage options to
|
||||
| persist session data. Database storage is a great default choice.
|
||||
|
|
||||
| Supported: "file", "cookie", "database", "apc",
|
||||
| "memcached", "redis", "dynamodb", "array"
|
||||
| Supported: "file", "cookie", "database", "memcached",
|
||||
| "redis", "dynamodb", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ return [
|
|||
| define the cache store which should be used to store the session data
|
||||
| between requests. This must match one of your defined cache stores.
|
||||
|
|
||||
| Affects: "apc", "dynamodb", "memcached", "redis"
|
||||
| Affects: "dynamodb", "memcached", "redis"
|
||||
|
|
||||
*/
|
||||
|
||||
|
|
@ -125,12 +125,11 @@ return [
|
|||
| the framework. Typically, you should not need to change this value
|
||||
| since doing so does not grant a meaningful security improvement.
|
||||
|
|
||||
|
|
||||
*/
|
||||
|
||||
'cookie' => env(
|
||||
'SESSION_COOKIE',
|
||||
Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
|
||||
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||
),
|
||||
|
||||
/*
|
||||
|
|
@ -153,7 +152,7 @@ return [
|
|||
|
|
||||
| This value determines the domain and subdomains the session cookie is
|
||||
| available to. By default, the cookie will be available to the root
|
||||
| domain and all subdomains. Typically, this shouldn't be changed.
|
||||
| domain without subdomains. Typically, this shouldn't be changed.
|
||||
|
|
||||
*/
|
||||
|
||||
|
|
@ -215,4 +214,20 @@ return [
|
|||
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Serialization
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value controls the serialization strategy for session data, which
|
||||
| is JSON by default. Setting this to "php" allows the storage of PHP
|
||||
| objects in the session but can make an application vulnerable to
|
||||
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
||||
|
|
||||
| Supported: "json", "php"
|
||||
|
|
||||
*/
|
||||
|
||||
'serialization' => 'json',
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
<?php
|
||||
|
||||
// You can find the keys here : https://dev.twitter.com/
|
||||
|
||||
return [
|
||||
'API_URL' => 'api.twitter.com',
|
||||
'API_VERSION' => '1.1',
|
||||
'AUTHENTICATE_URL' => 'https://api.twitter.com/oauth/authenticate',
|
||||
'AUTHORIZE_URL' => 'https://api.twitter.com/oauth/authorize',
|
||||
'ACCESS_TOKEN_URL' => 'oauth/access_token',
|
||||
'REQUEST_TOKEN_URL' => 'oauth/request_token',
|
||||
'USE_SSL' => true,
|
||||
|
||||
'CONSUMER_KEY' => env('TWITTER_CONSUMER_KEY'),
|
||||
'CONSUMER_SECRET' => env('TWITTER_CONSUMER_SECRET'),
|
||||
'ACCESS_TOKEN' => env('TWITTER_ACCESS_TOKEN'),
|
||||
'ACCESS_TOKEN_SECRET' => env('TWITTER_ACCESS_TOKEN_SECRET'),
|
||||
];
|
||||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
|||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Article>
|
||||
* @extends Factory<Article>
|
||||
*/
|
||||
class ArticleFactory extends Factory
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Bio;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Bio>
|
||||
* @extends Factory<Bio>
|
||||
*/
|
||||
class BioFactory extends Factory
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
|||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Bookmark>
|
||||
* @extends Factory<Bookmark>
|
||||
*/
|
||||
class BookmarkFactory extends Factory
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
|||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Contact>
|
||||
* @extends Factory<Contact>
|
||||
*/
|
||||
class ContactFactory extends Factory
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
|||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Like>
|
||||
* @extends Factory<Like>
|
||||
*/
|
||||
class LikeFactory extends Factory
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
|||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Media>
|
||||
* @extends Factory<Media>
|
||||
*/
|
||||
class MediaFactory extends Factory
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use App\Models\MicropubClient;
|
|||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\MicropubClient>
|
||||
* @extends Factory<MicropubClient>
|
||||
*/
|
||||
class MicropubClientFactory extends Factory
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
|||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Note>
|
||||
* @extends Factory<Note>
|
||||
*/
|
||||
class NoteFactory extends Factory
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use App\Models\Passkey;
|
|||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Passkey>
|
||||
* @extends Factory<Passkey>
|
||||
*/
|
||||
class PasskeyFactory extends Factory
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use App\Models\Place;
|
|||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Place>
|
||||
* @extends Factory<Place>
|
||||
*/
|
||||
class PlaceFactory extends Factory
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\SyndicationTarget;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\SyndicationTarget>
|
||||
* @extends Factory<SyndicationTarget>
|
||||
*/
|
||||
class SyndicationTargetFactory extends Factory
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use App\Models\Tag;
|
|||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Tag>
|
||||
* @extends Factory<Tag>
|
||||
*/
|
||||
class TagFactory extends Factory
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
|||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
* @extends Factory<User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use App\Models\WebMention;
|
|||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\WebMention>
|
||||
* @extends Factory<WebMention>
|
||||
*/
|
||||
class WebMentionFactory extends Factory
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
<?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('media_note', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('note_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('media_id')->constrained('media_endpoint')->onDelete('cascade');
|
||||
$table->text('alt_text')->nullable();
|
||||
$table->integer('order')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['note_id', 'media_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('media_note');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('webmentions', function (Blueprint $table) {
|
||||
$table->text('source')->change();
|
||||
$table->text('target')->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('webmentions', function (Blueprint $table) {
|
||||
$table->string('source')->change();
|
||||
$table->string('target')->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -723,8 +723,8 @@ ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;
|
|||
|
||||
CREATE TABLE public.webmentions (
|
||||
id integer NOT NULL,
|
||||
source character varying(255) NOT NULL,
|
||||
target character varying(255) NOT NULL,
|
||||
source text NOT NULL,
|
||||
target text NOT NULL,
|
||||
commentable_id integer,
|
||||
commentable_type character varying(255),
|
||||
type character varying(255),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ namespace Database\Seeders;
|
|||
|
||||
use App\Models\Like;
|
||||
use Faker\Generator;
|
||||
use Faker\Provider\en_US\Person;
|
||||
use Faker\Provider\Internet;
|
||||
use Faker\Provider\Lorem;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
|
@ -19,9 +22,9 @@ class LikesTableSeeder extends Seeder
|
|||
|
||||
$now = Carbon::now()->subDays(rand(3, 6));
|
||||
$faker = new Generator;
|
||||
$faker->addProvider(new \Faker\Provider\en_US\Person($faker));
|
||||
$faker->addProvider(new \Faker\Provider\Lorem($faker));
|
||||
$faker->addProvider(new \Faker\Provider\Internet($faker));
|
||||
$faker->addProvider(new Person($faker));
|
||||
$faker->addProvider(new Lorem($faker));
|
||||
$faker->addProvider(new Internet($faker));
|
||||
$likeFromAuthor = Like::create([
|
||||
'url' => $faker->url,
|
||||
'author_url' => $faker->url,
|
||||
|
|
|
|||
|
|
@ -84,7 +84,9 @@ class NotesTableSeeder extends Seeder
|
|||
// copy aaron’s profile pic in place
|
||||
$spl = new SplFileInfo(public_path().'/assets/profile-images/aaronparecki.com');
|
||||
if ($spl->isDir() === false) {
|
||||
mkdir(public_path() . '/assets/profile-images/aaronparecki.com', 0755);
|
||||
if (! mkdir($concurrentDirectory = public_path().'/assets/profile-images/aaronparecki.com', 0755) && ! is_dir($concurrentDirectory)) {
|
||||
throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
|
||||
}
|
||||
copy(base_path().'/tests/aaron.png', public_path().'/assets/profile-images/aaronparecki.com/image');
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +173,9 @@ class NotesTableSeeder extends Seeder
|
|||
$noteWithOnlyImage->setCreatedAt($now);
|
||||
$noteWithOnlyImage->setUpdatedAt($now);
|
||||
$noteWithOnlyImage->save();
|
||||
$noteWithOnlyImage->media()->save($media);
|
||||
$noteWithOnlyImage->media()->attach($media->id, [
|
||||
'alt_text' => 'Test alt text',
|
||||
]);
|
||||
DB::table('notes')
|
||||
->where('id', $noteWithOnlyImage->id)
|
||||
->update(['updated_at' => $now->toDateTimeString()]);
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
services:
|
||||
laravel.test:
|
||||
build:
|
||||
context: ./vendor/laravel/sail/runtimes/8.4
|
||||
context: ./vendor/laravel/sail/runtimes/8.5
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
WWWGROUP: '${WWWGROUP}'
|
||||
image: sail-8.4/app
|
||||
image: sail-8.5/app
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
ports:
|
||||
|
|
|
|||
BIN
jbuk_dev_backup.dump
Normal file
BIN
jbuk_dev_backup.dump
Normal file
Binary file not shown.
1545
package-lock.json
generated
1545
package-lock.json
generated
File diff suppressed because it is too large
Load diff
16
package.json
16
package.json
|
|
@ -6,15 +6,15 @@
|
|||
"repository": "https://github.com/jonnybarnes/jonnybarnes.uk",
|
||||
"license": "CC0-1.0",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.6.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@stylistic/eslint-plugin": "^5.1.0",
|
||||
"esbuild": "^0.27.1",
|
||||
"eslint": "^9.7.0",
|
||||
"globals": "^16.0.0",
|
||||
"esbuild": "^0.28.1",
|
||||
"eslint": "^10.0.1",
|
||||
"globals": "^17.3.0",
|
||||
"lightningcss": "^1.29.3",
|
||||
"lightningcss-cli": "^1.29.3",
|
||||
"stylelint": "^16.7.0",
|
||||
"stylelint-config-standard": "^39.0.0"
|
||||
"stylelint": "^17.3.0",
|
||||
"stylelint-config-standard": "^40.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"eslint": "eslint resources/js/*.js",
|
||||
|
|
@ -30,5 +30,9 @@
|
|||
"dependencies": {
|
||||
"@11ty/is-land": "^5.0.0",
|
||||
"@zachleat/snow-fall": "^1.0.3"
|
||||
},
|
||||
"allowScripts": {
|
||||
"esbuild@0.27.7": true,
|
||||
"lightningcss-cli@1.32.0": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
{
|
||||
"preset": "laravel",
|
||||
"rules": {
|
||||
"concat_space": false
|
||||
}
|
||||
"preset": "laravel"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
@layer reset{*,:before,:after{box-sizing:border-box}html{-webkit-text-size-adjust:none;-moz-text-size-adjust:none;text-size-adjust:none}body,h1,h2,h3,h4,p,figure,blockquote,dl,dd{margin-block-end:0}ul[role=list],ol[role=list]{list-style:none}body{line-height:1.5}h1,h2,h3,h4,button,input,label{line-height:1.1}h1,h2,h3,h4{text-wrap:balance}a:not([class]){text-decoration-skip-ink:auto;color:currentColor}img,picture{max-width:100%;display:block}input,button,textarea,select{font-family:inherit;font-size:inherit}textarea:not([rows]){min-height:10em}:target{scroll-margin-block:5ex}@media (prefers-reduced-motion){::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){animation:none!important}}}@layer base{:root{color-scheme:light dark;--primary-hue:307;--clr-text:light-dark(oklch(5% .1 var(--primary-hue)),oklch(100% .1 var(--primary-hue)));--clr-background:light-dark(oklch(100% .1 var(--primary-hue)),oklch(15% .15 var(--primary-hue)));--clr-border:light-dark(oklch(90% .2 var(--primary-hue)),oklch(25% .1 var(--primary-hue)));--clr-snow-fall:light-dark(oklch(25% .15 var(--primary-hue)),oklch(85% .2 var(--primary-hue)))}body{background-color:var(--clr-background);color:var(--clr-text)}:root{--border-radius:8px}html{font-size:125%}body{grid-template-rows:min-content 1fr min-content;grid-template-columns:1fr min(80ch,95vw) 1fr;padding-inline:4vw;display:grid;&>header{grid-column:1/-1}&>main{grid-column:2/3}&>footer{grid-column:1/-1;margin-block-start:2ex;& search form{flex-direction:row;align-items:center;gap:1ex;display:flex}}}.h-feed{flex-direction:column;gap:2ex;display:flex}}@layer components{body>header{grid-template-rows:auto auto;grid-template-columns:auto 1fr auto;align-items:baseline;gap:1rem;display:grid;& a{text-decoration:none}& h1{text-wrap:nowrap;margin:0}& nav{flex-direction:row;gap:.5rem;display:flex;@media screen and (width<=1100px){flex-wrap:wrap;grid-area:2/1/auto/span 3}}}.h-feed{& .note{border:1px solid var(--clr-border);border-radius:var(--border-radius);flex-direction:column;padding:1ex 2ex;display:flex;& .e-content>p:first-child{margin-block-start:0}& .syndication-links{flex-direction:row;gap:1ex;display:flex;& svg{border-radius:4px;width:auto;height:1lh}}}& .reply-to{border:1px solid var(--clr-border);border-top-left-radius:var(--border-radius);border-top-right-radius:var(--border-radius);border-bottom:none;margin-inline:2ex;padding-inline:1ex;font-size:85%}}.pagination{flex-direction:row;justify-content:center;align-items:center;display:flex;& div{flex-direction:row;gap:1ex;display:flex}}#theme-selector{& button{appearance:none;background:0 0;border:none;&:hover{cursor:pointer}}& #theme-selector-dropdown{position-area:block-end span-inline-start;border:2px solid var(--clr-border);background-color:var(--clr-background);color:var(--clr-text);margin:0;position:absolute;& fieldset{border:0;flex-direction:row;gap:1ch;display:flex;& input{display:none}}}@media (prefers-reduced-motion:no-preference){& #theme-selector-dropdown{&:popover-open{opacity:1;transform:translateY(0)scale(1);@starting-style{opacity:0;transform:translateY(30px)scale(0)}}opacity:0;transition:transform,opacity,display allow-discrete,overlay allow-discrete;transform-origin:100% 0;transition-duration:.5s;transform:translateY(0)scale(0)}}}::view-transition-group(changing-theme){animation-duration:1.25s}::view-transition-new(changing-theme),::view-transition-old(changing-theme){mix-blend-mode:normal}::view-transition-new(changing-theme){animation-name:reveal}::view-transition-old(changing-theme){animation:none}@keyframes reveal{0%{clip-path:polygon(-30% 0,-30% 0,-15% 100%,-10% 115%)}to{clip-path:polygon(-30% 0,130% 0,115% 100%,-10% 115%)}}}
|
||||
@layer reset{*,:before,:after{box-sizing:border-box}html{-webkit-text-size-adjust:none;-moz-text-size-adjust:none;text-size-adjust:none}body,h1,h2,h3,h4,p,figure,blockquote,dl,dd{margin-block-end:0}ul[role=list],ol[role=list]{list-style:none}body{line-height:1.5}h1,h2,h3,h4,button,input,label{line-height:1.1}h1,h2,h3,h4{text-wrap:balance}a:not([class]){text-decoration-skip-ink:auto;color:currentColor}img,picture{max-width:100%;height:auto;display:block}input,button,textarea,select{font-family:inherit;font-size:inherit}textarea:not([rows]){min-height:10em}:target{scroll-margin-block:5ex}@media (prefers-reduced-motion){::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){animation:none!important}}}@layer base{:root{color-scheme:light dark;--primary-hue:307;--clr-text:light-dark(oklch(5% .1 var(--primary-hue)),oklch(100% .1 var(--primary-hue)));--clr-background:light-dark(oklch(100% .1 var(--primary-hue)),oklch(15% .15 var(--primary-hue)));--clr-border:light-dark(oklch(90% .2 var(--primary-hue)),oklch(25% .1 var(--primary-hue)));--clr-snow-fall:light-dark(oklch(25% .15 var(--primary-hue)),oklch(85% .2 var(--primary-hue)))}body{background-color:var(--clr-background);color:var(--clr-text)}:root{--border-radius:8px}html{font-size:125%}body{grid-template-rows:min-content 1fr min-content;grid-template-columns:1fr min(80ch,80vw) 1fr;padding-inline:4vw;display:grid;&>header{grid-column:1/-1}&>main{grid-column:2/3}&>footer{grid-column:1/-1;margin-block-start:2ex;& search form{flex-direction:row;align-items:center;gap:1ex;display:flex;& input{min-width:0}}}}.h-feed{flex-direction:column;gap:2ex;display:flex}}@layer components{.h-entry{& pre{padding:1rem}}body>header{grid-template-rows:auto auto;grid-template-columns:auto 1fr auto;align-items:baseline;gap:1rem;display:grid;& a{text-decoration:none}& h1{text-wrap:nowrap;margin:0}& nav{flex-direction:row;gap:.5rem;display:flex;@media screen and (width<=1100px){flex-wrap:wrap;grid-area:2/1/auto/span 3}}}.note{border:1px solid var(--clr-border);border-radius:var(--border-radius);flex-direction:column;padding:1ex 2ex;display:flex;& .e-content{&>p:first-child{margin-block-start:0}& .u-photo{width:100%;height:auto}}& .syndication-links{flex-direction:row;gap:1ex;display:flex;& svg{border-radius:4px;width:auto;height:1lh}}}.reply-to{border:1px solid var(--clr-border);border-top-left-radius:var(--border-radius);border-top-right-radius:var(--border-radius);border-bottom:none;margin-inline:2ex;padding-inline:1ex;font-size:85%}.pagination{flex-direction:row;justify-content:center;align-items:center;display:flex;& div{flex-direction:row;gap:1ex;display:flex}}#theme-selector{& button{appearance:none;background:0 0;border:none;&:hover{cursor:pointer}}& #theme-selector-dropdown{position-area:block-end span-inline-start;border:2px solid var(--clr-border);background-color:var(--clr-background);color:var(--clr-text);margin:0;position:absolute;& fieldset{border:0;flex-direction:row;gap:1ch;display:flex;& input{display:none}}}@media (prefers-reduced-motion:no-preference){& #theme-selector-dropdown{&:popover-open{opacity:1;transform:translateY(0)scale(1);@starting-style{opacity:0;transform:translateY(30px)scale(0)}}opacity:0;transition:transform, opacity, display allow-discrete, overlay allow-discrete;transform-origin:100% 0;transition-duration:.5s;transform:translateY(0)scale(0)}}}::view-transition-group(changing-theme){animation-duration:1.25s}::view-transition-new(changing-theme),::view-transition-old(changing-theme){mix-blend-mode:normal}::view-transition-new(changing-theme){animation-name:reveal}::view-transition-old(changing-theme){animation:none}@keyframes reveal{0%{clip-path:polygon(-30% 0,-30% 0,-15% 100%,-10% 115%)}to{clip-path:polygon(-30% 0,130% 0,115% 100%,-10% 115%)}}}
|
||||
/*# sourceMappingURL=/assets/css/app.css.map */
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
87
public/assets/highlight/nord.css
Normal file
87
public/assets/highlight/nord.css
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
pre, code {
|
||||
color: #d8dee9;
|
||||
background-color: #2e3440;
|
||||
}
|
||||
|
||||
.hl-keyword {
|
||||
color: #81A1C1;
|
||||
}
|
||||
|
||||
.hl-property {
|
||||
color: #88C0D0;
|
||||
}
|
||||
|
||||
.hl-attribute {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hl-type {
|
||||
color: #8FBCBB;
|
||||
}
|
||||
|
||||
.hl-generic {
|
||||
color: #81A1C1;
|
||||
}
|
||||
|
||||
.hl-value {
|
||||
color: #A3BE8C;
|
||||
}
|
||||
|
||||
.hl-literal {
|
||||
color: #A3BE8C;
|
||||
}
|
||||
|
||||
.hl-number {
|
||||
color: #B48EAD;
|
||||
}
|
||||
|
||||
.hl-variable {
|
||||
color: #D8DEE9;
|
||||
}
|
||||
|
||||
.hl-comment {
|
||||
color: #616E88;
|
||||
}
|
||||
|
||||
.hl-blur {
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.hl-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hl-em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hl-addition {
|
||||
display: inline-block;
|
||||
min-width: 100%;
|
||||
background-color: #00FF0022;
|
||||
}
|
||||
|
||||
.hl-deletion {
|
||||
display: inline-block;
|
||||
min-width: 100%;
|
||||
background-color: #FF000011;
|
||||
}
|
||||
|
||||
.hl-gutter {
|
||||
display: inline-block;
|
||||
font-size: 0.9em;
|
||||
color: #555;
|
||||
padding: 0 1ch;
|
||||
margin-right: 1ch;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.hl-gutter-addition {
|
||||
background-color: #34A853;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hl-gutter-deletion {
|
||||
background-color: #EA4334;
|
||||
color: #fff;
|
||||
}
|
||||
BIN
public/assets/highlight/nord.css.br
Normal file
BIN
public/assets/highlight/nord.css.br
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue