Compare commits
117 changed files with 2215 additions and 1878 deletions
|
|
@ -70,6 +70,11 @@ ADMIN_USER=admin# pick something better, this is used for `/admin`
|
||||||
ADMIN_PASS=password
|
ADMIN_PASS=password
|
||||||
DISPLAY_NAME='Joe Bloggs'# This is used for example in the header and titles
|
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_DRIVER=database
|
||||||
SCOUT_QUEUE=false
|
SCOUT_QUEUE=false
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ namespace App\Http\Controllers\Admin;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Contact;
|
use App\Models\Contact;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use GuzzleHttp\Exception\BadResponseException;
|
|
||||||
use Illuminate\Filesystem\Filesystem;
|
use Illuminate\Filesystem\Filesystem;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
|
@ -104,7 +103,7 @@ class ContactsController extends Controller
|
||||||
* This method attempts to find the microformat marked-up profile image
|
* This method attempts to find the microformat marked-up profile image
|
||||||
* from a given homepage and save it accordingly
|
* from a given homepage and save it accordingly
|
||||||
*
|
*
|
||||||
* @return RedirectResponse|View
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||||
*/
|
*/
|
||||||
public function getAvatar(int $contactId)
|
public function getAvatar(int $contactId)
|
||||||
{
|
{
|
||||||
|
|
@ -116,7 +115,7 @@ class ContactsController extends Controller
|
||||||
$client = resolve(Client::class);
|
$client = resolve(Client::class);
|
||||||
try {
|
try {
|
||||||
$response = $client->get($contact->homepage);
|
$response = $client->get($contact->homepage);
|
||||||
} catch (BadResponseException $e) {
|
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
|
||||||
return redirect('/admin/contacts/' . $contactId . '/edit')
|
return redirect('/admin/contacts/' . $contactId . '/edit')
|
||||||
->with('error', 'Bad resposne from contact’s homepage');
|
->with('error', 'Bad resposne from contact’s homepage');
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +129,7 @@ class ContactsController extends Controller
|
||||||
if ($avatarURL !== null) {
|
if ($avatarURL !== null) {
|
||||||
try {
|
try {
|
||||||
$avatar = $client->get($avatarURL);
|
$avatar = $client->get($avatarURL);
|
||||||
} catch (BadResponseException $e) {
|
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
|
||||||
return redirect('/admin/contacts/' . $contactId . '/edit')
|
return redirect('/admin/contacts/' . $contactId . '/edit')
|
||||||
->with('error', 'Unable to download avatar');
|
->with('error', 'Unable to download avatar');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ class PasskeysController extends Controller
|
||||||
// Unset session data to mitigate replay attacks
|
// Unset session data to mitigate replay attacks
|
||||||
$request->session()->forget('create_options');
|
$request->session()->forget('create_options');
|
||||||
if (empty($publicKeyCredentialCreationOptionsData)) {
|
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;
|
$attestationStatementSupportManager = new AttestationStatementSupportManager;
|
||||||
|
|
@ -145,7 +145,7 @@ class PasskeysController extends Controller
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! $publicKeyCredential->response instanceof AuthenticatorAttestationResponse) {
|
if (! $publicKeyCredential->response instanceof AuthenticatorAttestationResponse) {
|
||||||
throw new WebauthnException('Invalid response type');
|
throw new WebAuthnException('Invalid response type');
|
||||||
}
|
}
|
||||||
|
|
||||||
$algorithmManager = new Manager;
|
$algorithmManager = new Manager;
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,6 @@ class PlacesController extends Controller
|
||||||
foreach ($place1->notes as $note) {
|
foreach ($place1->notes as $note) {
|
||||||
$note->place()->dissociate();
|
$note->place()->dissociate();
|
||||||
$note->place()->associate($place2->id);
|
$note->place()->associate($place2->id);
|
||||||
$note->save();
|
|
||||||
}
|
}
|
||||||
$place1->delete();
|
$place1->delete();
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +127,6 @@ class PlacesController extends Controller
|
||||||
foreach ($place2->notes as $note) {
|
foreach ($place2->notes as $note) {
|
||||||
$note->place()->dissociate();
|
$note->place()->dissociate();
|
||||||
$note->place()->associate($place1->id);
|
$note->place()->associate($place1->id);
|
||||||
$note->save();
|
|
||||||
}
|
}
|
||||||
$place2->delete();
|
$place2->delete();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class RedirectIfAuthenticated
|
||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* Handle an incoming request.
|
||||||
*
|
*
|
||||||
* @param Closure(Request): (Response) $next
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next, string ...$guards): Response
|
public function handle(Request $request, Closure $next, string ...$guards): Response
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@ declare(strict_types=1);
|
||||||
namespace App\Jobs;
|
namespace App\Jobs;
|
||||||
|
|
||||||
use App\Models\Like;
|
use App\Models\Like;
|
||||||
|
use Codebird\Codebird;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use GuzzleHttp\Exception\GuzzleException;
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
|
use GuzzleHttp\Exception\RequestException;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
|
@ -37,6 +39,35 @@ class ProcessLike implements ShouldQueue
|
||||||
*/
|
*/
|
||||||
public function handle(Client $client, Authorship $authorship): int
|
public function handle(Client $client, 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);
|
$response = $client->request('GET', $this->like->url);
|
||||||
$mf2 = \Mf2\parse((string) $response->getBody(), $this->like->url);
|
$mf2 = \Mf2\parse((string) $response->getBody(), $this->like->url);
|
||||||
if (Arr::has($mf2, 'items.0.properties.content')) {
|
if (Arr::has($mf2, 'items.0.properties.content')) {
|
||||||
|
|
@ -60,4 +91,15 @@ class ProcessLike implements ShouldQueue
|
||||||
|
|
||||||
return 0;
|
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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Mf2\Parser;
|
|
||||||
|
|
||||||
class SendWebMentions implements ShouldQueue
|
class SendWebMentions implements ShouldQueue
|
||||||
{
|
{
|
||||||
|
|
@ -84,11 +83,7 @@ class SendWebMentions implements ShouldQueue
|
||||||
// failed to find a header so parse HTML
|
// failed to find a header so parse HTML
|
||||||
$html = (string) $response->getBody();
|
$html = (string) $response->getBody();
|
||||||
|
|
||||||
if ($html === '') {
|
$mf2 = new \Mf2\Parser($html, $url);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$mf2 = new Parser($html, $url);
|
|
||||||
$rels = $mf2->parseRelsAndAlternates();
|
$rels = $mf2->parseRelsAndAlternates();
|
||||||
if (array_key_exists('webmention', $rels[0])) {
|
if (array_key_exists('webmention', $rels[0])) {
|
||||||
$endpoint = $rels[0]['webmention'][0];
|
$endpoint = $rels[0]['webmention'][0];
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ declare(strict_types=1);
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Cviebrock\EloquentSluggable\Sluggable;
|
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\Builder;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -20,14 +18,23 @@ use League\CommonMark\MarkdownConverter;
|
||||||
use Spatie\CommonMarkHighlighter\FencedCodeRenderer;
|
use Spatie\CommonMarkHighlighter\FencedCodeRenderer;
|
||||||
use Spatie\CommonMarkHighlighter\IndentedCodeRenderer;
|
use Spatie\CommonMarkHighlighter\IndentedCodeRenderer;
|
||||||
|
|
||||||
#[Table('articles')]
|
|
||||||
#[Fillable(['url', 'title', 'main', 'published'])]
|
|
||||||
class Article extends Model
|
class Article extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use Sluggable;
|
use Sluggable;
|
||||||
use SoftDeletes;
|
use SoftDeletes;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
protected $table = 'articles';
|
||||||
|
|
||||||
|
/** @var array<int, string> */
|
||||||
|
protected $fillable = [
|
||||||
|
'url',
|
||||||
|
'title',
|
||||||
|
'main',
|
||||||
|
'published',
|
||||||
|
];
|
||||||
|
|
||||||
/** @var array<string, string> */
|
/** @var array<string, string> */
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'created_at' => 'datetime',
|
'created_at' => 'datetime',
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,18 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
|
||||||
#[Fillable(['url', 'name', 'content'])]
|
|
||||||
class Bookmark extends Model
|
class Bookmark extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
/** @var array<int, string> */
|
||||||
|
protected $fillable = ['url', 'name', 'content'];
|
||||||
|
|
||||||
/** @var array<string, string> */
|
/** @var array<string, string> */
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'syndicates' => 'array',
|
'syndicates' => 'array',
|
||||||
|
|
@ -25,7 +26,7 @@ class Bookmark extends Model
|
||||||
return $this->belongsToMany('App\Models\Tag');
|
return $this->belongsToMany('App\Models\Tag');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function localUri(): Attribute
|
protected function local_uri(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::get(
|
return Attribute::get(
|
||||||
get: fn () => config('app.url') . '/bookmarks/' . $this->id,
|
get: fn () => config('app.url') . '/bookmarks/' . $this->id,
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,20 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
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\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
#[Table('contacts')]
|
|
||||||
#[Fillable(['nick', 'name', 'homepage', 'twitter', 'facebook'])]
|
|
||||||
class Contact extends Model
|
class Contact extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
protected $table = 'contacts';
|
||||||
|
|
||||||
|
/** @var array<int, string> */
|
||||||
|
protected $fillable = ['nick', 'name', 'homepage', 'twitter', 'facebook'];
|
||||||
|
|
||||||
protected function photo(): Attribute
|
protected function photo(): Attribute
|
||||||
{
|
{
|
||||||
$photo = '/assets/profile-images/default-image';
|
$photo = '/assets/profile-images/default-image';
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,20 @@ declare(strict_types=1);
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Traits\FilterHtml;
|
use App\Traits\FilterHtml;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Mf2;
|
use Mf2;
|
||||||
|
|
||||||
#[Fillable(['url'])]
|
|
||||||
class Like extends Model
|
class Like extends Model
|
||||||
{
|
{
|
||||||
use FilterHtml;
|
use FilterHtml;
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
/** @var array<int, string> */
|
||||||
|
protected $fillable = ['url'];
|
||||||
|
|
||||||
protected function url(): Attribute
|
protected function url(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::set(
|
return Attribute::set(
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,22 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
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\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
#[Table('media_endpoint')]
|
|
||||||
#[Fillable(['token', 'path', 'type', 'image_widths'])]
|
|
||||||
class Media extends Model
|
class Media extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
protected $table = 'media_endpoint';
|
||||||
|
|
||||||
|
/** @var array<int, string> */
|
||||||
|
protected $fillable = ['token', 'path', 'type', 'image_widths'];
|
||||||
|
|
||||||
public function notes(): BelongsToMany
|
public function notes(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Note::class)
|
return $this->belongsToMany(Note::class)
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,20 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
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\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
#[Table('clients')]
|
|
||||||
#[Fillable(['client_url', 'client_name'])]
|
|
||||||
class MicropubClient extends Model
|
class MicropubClient extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
protected $table = 'clients';
|
||||||
|
|
||||||
|
/** @var array<int, string> */
|
||||||
|
protected $fillable = ['client_url', 'client_name'];
|
||||||
|
|
||||||
public function notes(): HasMany
|
public function notes(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany('App\Models\Note', 'client_id', 'client_url');
|
return $this->hasMany('App\Models\Note', 'client_id', 'client_url');
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,9 @@ namespace App\Models;
|
||||||
|
|
||||||
use App\CommonMark\Generators\MentionGenerator;
|
use App\CommonMark\Generators\MentionGenerator;
|
||||||
use App\CommonMark\Renderers\MentionRenderer;
|
use App\CommonMark\Renderers\MentionRenderer;
|
||||||
use App\Observers\NoteObserver;
|
use Codebird\Codebird;
|
||||||
|
use Exception;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
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\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
@ -34,10 +31,6 @@ use Normalizer;
|
||||||
use Spatie\CommonMarkHighlighter\FencedCodeRenderer;
|
use Spatie\CommonMarkHighlighter\FencedCodeRenderer;
|
||||||
use Spatie\CommonMarkHighlighter\IndentedCodeRenderer;
|
use Spatie\CommonMarkHighlighter\IndentedCodeRenderer;
|
||||||
|
|
||||||
#[Table('notes')]
|
|
||||||
#[Fillable(['note', 'in_reply_to', 'client_id'])]
|
|
||||||
#[Hidden(['searchable'])]
|
|
||||||
#[ObservedBy(NoteObserver::class)]
|
|
||||||
class Note extends Model
|
class Note extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
@ -68,6 +61,16 @@ class Note extends Model
|
||||||
/** @var string */
|
/** @var string */
|
||||||
protected $table = 'notes';
|
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
|
public function tags(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Tag::class);
|
return $this->belongsToMany(Tag::class);
|
||||||
|
|
@ -238,6 +241,43 @@ class Note extends Model
|
||||||
return null;
|
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.
|
* Scope a query to select a note via a NewBase60 id.
|
||||||
*/
|
*/
|
||||||
|
|
@ -419,4 +459,9 @@ class Note extends Model
|
||||||
return $address;
|
return $address;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function isTwitterLink(string $inReplyTo): bool
|
||||||
|
{
|
||||||
|
return str_starts_with($inReplyTo, 'https://twitter.com/');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,20 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
#[Fillable(['passkey_id', 'passkey'])]
|
|
||||||
class Passkey extends Model
|
class Passkey extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
/** @inerhitDoc */
|
||||||
|
protected $fillable = [
|
||||||
|
'passkey_id',
|
||||||
|
'passkey',
|
||||||
|
];
|
||||||
|
|
||||||
public function user(): BelongsTo
|
public function user(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Cviebrock\EloquentSluggable\Sluggable;
|
use Cviebrock\EloquentSluggable\Sluggable;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -13,7 +12,6 @@ use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
#[Fillable(['name', 'slug'])]
|
|
||||||
class Place extends Model
|
class Place extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
@ -24,6 +22,9 @@ class Place extends Model
|
||||||
return 'slug';
|
return 'slug';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @var array<int, string> */
|
||||||
|
protected $fillable = ['name', 'slug'];
|
||||||
|
|
||||||
/** @var array<string, string> */
|
/** @var array<string, string> */
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'latitude' => 'float',
|
'latitude' => 'float',
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,40 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
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\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
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
|
class SyndicationTarget extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
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
|
protected function service(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::get(
|
return Attribute::get(
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,19 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
#[Guarded(['id'])]
|
|
||||||
class Tag extends Model
|
class Tag extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
/** @var array<int, string> */
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
public function notes(): BelongsToMany
|
public function notes(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Note::class);
|
return $this->belongsToMany(Note::class);
|
||||||
|
|
|
||||||
|
|
@ -4,34 +4,27 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
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\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
#[Fillable(['name', 'password'])]
|
|
||||||
#[Hidden(['password', 'remember_token'])]
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<UserFactory> */
|
use HasFactory;
|
||||||
use HasFactory, Notifiable;
|
use Notifiable;
|
||||||
|
|
||||||
/**
|
/** @var array<int, string> */
|
||||||
* Get the attributes that should be cast.
|
protected $fillable = [
|
||||||
*
|
'name', 'password',
|
||||||
* @return array<string, string>
|
];
|
||||||
*/
|
|
||||||
protected function casts(): array
|
/** @var array<int, string> */
|
||||||
{
|
protected $hidden = [
|
||||||
return [
|
'current_password',
|
||||||
'email_verified_at' => 'datetime',
|
'password',
|
||||||
'password' => 'hashed',
|
'remember_token',
|
||||||
];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
public function passkey(): HasMany
|
public function passkey(): HasMany
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,27 @@ declare(strict_types=1);
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Traits\FilterHtml;
|
use App\Traits\FilterHtml;
|
||||||
|
use Codebird\Codebird;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Table;
|
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
use Illuminate\Filesystem\Filesystem;
|
use Illuminate\Filesystem\Filesystem;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Jonnybarnes\WebmentionsParser\Authorship;
|
use Jonnybarnes\WebmentionsParser\Authorship;
|
||||||
|
|
||||||
#[Table('webmentions')]
|
|
||||||
#[Guarded(['id'])]
|
|
||||||
class WebMention extends Model
|
class WebMention extends Model
|
||||||
{
|
{
|
||||||
use FilterHtml;
|
use FilterHtml;
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
protected $table = 'webmentions';
|
||||||
|
|
||||||
|
/** @var array<int, string> */
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
public function commentable(): MorphTo
|
public function commentable(): MorphTo
|
||||||
{
|
{
|
||||||
return $this->morphTo();
|
return $this->morphTo();
|
||||||
|
|
@ -123,6 +127,19 @@ class WebMention extends Model
|
||||||
return str_replace('http://', 'https://', $url);
|
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;
|
$filesystem = new Filesystem;
|
||||||
if ($filesystem->exists(public_path() . '/assets/profile-images/' . $host . '/image')) {
|
if ($filesystem->exists(public_path() . '/assets/profile-images/' . $host . '/image')) {
|
||||||
return '/assets/profile-images/' . $host . '/image';
|
return '/assets/profile-images/' . $host . '/image';
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,15 @@
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Models\Note;
|
||||||
|
use App\Observers\NoteObserver;
|
||||||
|
use Codebird\Codebird;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use GuzzleHttp\Exception\ConnectException;
|
|
||||||
use GuzzleHttp\HandlerStack;
|
|
||||||
use GuzzleHttp\Middleware;
|
use GuzzleHttp\Middleware;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Intervention\Image\ImageManager;
|
|
||||||
use Lcobucci\JWT\Configuration;
|
use Lcobucci\JWT\Configuration;
|
||||||
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||||
use Lcobucci\JWT\Signer\Key\InMemory;
|
use Lcobucci\JWT\Signer\Key\InMemory;
|
||||||
|
|
@ -33,11 +33,33 @@ class AppServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
|
Note::observe(NoteObserver::class);
|
||||||
|
|
||||||
// configure Intervention/Image
|
// configure Intervention/Image
|
||||||
$this->app->bind('Intervention\Image\ImageManager', function () {
|
$this->app->bind('Intervention\Image\ImageManager', function () {
|
||||||
return ImageManager::withDriver(config('image.driver'));
|
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.
|
* Paginate a standard Laravel Collection.
|
||||||
*
|
*
|
||||||
|
|
@ -84,7 +106,7 @@ class AppServiceProvider extends ServiceProvider
|
||||||
|
|
||||||
// Configure Guzzle
|
// Configure Guzzle
|
||||||
$this->app->bind('RetryGuzzle', function () {
|
$this->app->bind('RetryGuzzle', function () {
|
||||||
$handlerStack = HandlerStack::create();
|
$handlerStack = \GuzzleHttp\HandlerStack::create();
|
||||||
$handlerStack->push(Middleware::retry(
|
$handlerStack->push(Middleware::retry(
|
||||||
function ($retries, $request, $response, $exception) {
|
function ($retries, $request, $response, $exception) {
|
||||||
// Limit the number of retries to 5
|
// Limit the number of retries to 5
|
||||||
|
|
@ -93,7 +115,7 @@ class AppServiceProvider extends ServiceProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry connection exceptions
|
// Retry connection exceptions
|
||||||
if ($exception instanceof ConnectException) {
|
if ($exception instanceof \GuzzleHttp\Exception\ConnectException) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,14 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withMiddleware(function (Middleware $middleware) {
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
$middleware
|
$middleware
|
||||||
->append(LinkHeadersMiddleware::class)
|
->append(LinkHeadersMiddleware::class)
|
||||||
->preventRequestForgery(
|
->validateCsrfTokens(except: [
|
||||||
except: [
|
|
||||||
'auth', // This is the IndieAuth auth endpoint
|
'auth', // This is the IndieAuth auth endpoint
|
||||||
'token', // This is the IndieAuth token endpoint
|
'token', // This is the IndieAuth token endpoint
|
||||||
'api/post',
|
'api/post',
|
||||||
'api/media',
|
'api/media',
|
||||||
'micropub/places',
|
'micropub/places',
|
||||||
'webmention',
|
'webmention',
|
||||||
],
|
]);
|
||||||
originOnly: true
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions) {
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
Flare::handles($exceptions);
|
Flare::handles($exceptions);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Providers\AppServiceProvider;
|
|
||||||
use App\Providers\HorizonServiceProvider;
|
|
||||||
use App\Providers\MicropubServiceProvider;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
AppServiceProvider::class,
|
App\Providers\AppServiceProvider::class,
|
||||||
HorizonServiceProvider::class,
|
App\Providers\HorizonServiceProvider::class,
|
||||||
MicropubServiceProvider::class,
|
App\Providers\MicropubServiceProvider::class,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -6,30 +6,34 @@
|
||||||
"keywords": ["laravel", "framework", "indieweb"],
|
"keywords": ["laravel", "framework", "indieweb"],
|
||||||
"license": "CC0-1.0",
|
"license": "CC0-1.0",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.3",
|
"php": "^8.2",
|
||||||
"ext-dom": "*",
|
"ext-dom": "*",
|
||||||
"ext-intl": "*",
|
"ext-intl": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-pgsql": "*",
|
"ext-pgsql": "*",
|
||||||
"ext-pdo": "*",
|
|
||||||
"ext-sodium": "*",
|
"ext-sodium": "*",
|
||||||
"cviebrock/eloquent-sluggable": "^13.0",
|
"cviebrock/eloquent-sluggable": "^12.0",
|
||||||
"guzzlehttp/guzzle": "^7.2",
|
"guzzlehttp/guzzle": "^7.2",
|
||||||
"indieauth/client": "^1.1",
|
"indieauth/client": "^1.1",
|
||||||
"intervention/image": "^3",
|
"intervention/image": "^3",
|
||||||
"jonnybarnes/indieweb": "~0.2",
|
"jonnybarnes/indieweb": "~0.2",
|
||||||
"jonnybarnes/webmentions-parser": "~0.5",
|
"jonnybarnes/webmentions-parser": "~0.5",
|
||||||
"laravel/framework": "^13.0",
|
"jublonet/codebird-php": "4.0.0-beta.1",
|
||||||
|
"laravel/framework": "^12.0",
|
||||||
"laravel/horizon": "^5.0",
|
"laravel/horizon": "^5.0",
|
||||||
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/scout": "^10.1",
|
"laravel/scout": "^10.1",
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^2.8",
|
||||||
"lcobucci/jwt": "^5.0",
|
"lcobucci/jwt": "^5.0",
|
||||||
"league/commonmark": "^2.0",
|
"league/commonmark": "^2.0",
|
||||||
"league/flysystem-aws-s3-v3": "^3.0",
|
"league/flysystem-aws-s3-v3": "^3.0",
|
||||||
"mf2/mf2": "~0.3",
|
"mf2/mf2": "~0.3",
|
||||||
|
"phpdocumentor/reflection-docblock": "^5.3",
|
||||||
"spatie/commonmark-highlighter": "^3.0",
|
"spatie/commonmark-highlighter": "^3.0",
|
||||||
"spatie/laravel-flare": "^2.2",
|
"spatie/laravel-flare": "^2.2",
|
||||||
"symfony/html-sanitizer": "^8.0",
|
"symfony/html-sanitizer": "^7.0",
|
||||||
|
"symfony/property-access": "^7.0",
|
||||||
|
"symfony/serializer": "^7.0",
|
||||||
"web-auth/webauthn-lib": "^5.0"
|
"web-auth/webauthn-lib": "^5.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
|
@ -37,13 +41,13 @@
|
||||||
"barryvdh/laravel-ide-helper": "^3.0",
|
"barryvdh/laravel-ide-helper": "^3.0",
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
"laravel/dusk": "^8.0",
|
"laravel/dusk": "^8.0",
|
||||||
"laravel/pail": "^1.2.5",
|
"laravel/pail": "^1.2.2",
|
||||||
"laravel/pint": "^1.27",
|
"laravel/pint": "^1.24",
|
||||||
"laravel/sail": "^1.41",
|
"laravel/sail": "^1.41",
|
||||||
"mockery/mockery": "^1.6",
|
"mockery/mockery": "^1.4.4",
|
||||||
"nunomaduro/collision": "^8.6",
|
"nunomaduro/collision": "^8.6",
|
||||||
"phpunit/php-code-coverage": "^12.0",
|
"phpunit/php-code-coverage": "^11.0",
|
||||||
"phpunit/phpunit": "^12.5.12",
|
"phpunit/phpunit": "^11.5.50",
|
||||||
"spatie/laravel-ray": "^1.12",
|
"spatie/laravel-ray": "^1.12",
|
||||||
"spatie/x-ray": "^1.2"
|
"spatie/x-ray": "^1.2"
|
||||||
},
|
},
|
||||||
|
|
@ -68,7 +72,7 @@
|
||||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||||
"@php artisan key:generate",
|
"@php artisan key:generate",
|
||||||
"@php artisan migrate --force",
|
"@php artisan migrate --force",
|
||||||
"npm install --ignore-scripts",
|
"npm install",
|
||||||
"npm run build"
|
"npm run build"
|
||||||
],
|
],
|
||||||
"dev": [
|
"dev": [
|
||||||
|
|
|
||||||
2269
composer.lock
generated
2269
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -64,7 +62,7 @@ return [
|
||||||
'providers' => [
|
'providers' => [
|
||||||
'users' => [
|
'users' => [
|
||||||
'driver' => 'eloquent',
|
'driver' => 'eloquent',
|
||||||
'model' => env('AUTH_MODEL', User::class),
|
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||||
],
|
],
|
||||||
|
|
||||||
// 'users' => [
|
// 'users' => [
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,9 @@ return [
|
||||||
|
|
||||||
'database' => [
|
'database' => [
|
||||||
'driver' => 'database',
|
'driver' => 'database',
|
||||||
'connection' => env('DB_CACHE_CONNECTION'),
|
|
||||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||||
|
'connection' => env('DB_CACHE_CONNECTION'),
|
||||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
|
||||||
],
|
],
|
||||||
|
|
||||||
'file' => [
|
'file' => [
|
||||||
|
|
@ -112,19 +111,6 @@ return [
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) 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,7 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Pdo\Mysql;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
|
|
@ -60,7 +59,7 @@ return [
|
||||||
'strict' => true,
|
'strict' => true,
|
||||||
'engine' => null,
|
'engine' => null,
|
||||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||||
]) : [],
|
]) : [],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
@ -80,7 +79,7 @@ return [
|
||||||
'strict' => true,
|
'strict' => true,
|
||||||
'engine' => null,
|
'engine' => null,
|
||||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||||
]) : [],
|
]) : [],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
@ -96,7 +95,7 @@ return [
|
||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'prefix_indexes' => true,
|
'prefix_indexes' => true,
|
||||||
'search_path' => 'public',
|
'search_path' => 'public',
|
||||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
'sslmode' => 'prefer',
|
||||||
],
|
],
|
||||||
|
|
||||||
'sqlsrv' => [
|
'sqlsrv' => [
|
||||||
|
|
@ -149,7 +148,7 @@ return [
|
||||||
|
|
||||||
'options' => [
|
'options' => [
|
||||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'_database_'),
|
||||||
'persistent' => env('REDIS_PERSISTENT', false),
|
'persistent' => env('REDIS_PERSISTENT', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ return [
|
||||||
| may even configure multiple disks for the same driver. Examples for
|
| may even configure multiple disks for the same driver. Examples for
|
||||||
| most supported storage drivers are configured here for reference.
|
| 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' => [
|
'public' => [
|
||||||
'driver' => 'local',
|
'driver' => 'local',
|
||||||
'root' => storage_path('app/public'),
|
'root' => storage_path('app/public'),
|
||||||
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
'url' => rtrim(env('APP_URL'), '/').'/storage',
|
||||||
'visibility' => 'public',
|
'visibility' => 'public',
|
||||||
'throw' => false,
|
'throw' => false,
|
||||||
'report' => false,
|
'report' => false,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Spatie\FlareClient\Api;
|
use Spatie\FlareClient\Api;
|
||||||
use Spatie\FlareClient\Sampling\RateSampler;
|
|
||||||
use Spatie\LaravelFlare\AttributesProviders\LaravelUserAttributesProvider;
|
use Spatie\LaravelFlare\AttributesProviders\LaravelUserAttributesProvider;
|
||||||
use Spatie\LaravelFlare\FlareConfig;
|
use Spatie\LaravelFlare\FlareConfig;
|
||||||
use Spatie\LaravelFlare\Senders\LaravelHttpSender;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
/*
|
/*
|
||||||
|
|
@ -154,7 +152,7 @@ return [
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'sender' => [
|
'sender' => [
|
||||||
'class' => LaravelHttpSender::class,
|
'class' => \Spatie\LaravelFlare\Senders\LaravelHttpSender::class,
|
||||||
'config' => [
|
'config' => [
|
||||||
'timeout' => 10,
|
'timeout' => 10,
|
||||||
],
|
],
|
||||||
|
|
@ -184,7 +182,7 @@ return [
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
'sampler' => [
|
'sampler' => [
|
||||||
'class' => RateSampler::class,
|
'class' => \Spatie\FlareClient\Sampling\RateSampler::class,
|
||||||
'config' => [
|
'config' => [
|
||||||
'rate' => 0.1,
|
'rate' => 0.1,
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Monolog\Handler\NullHandler;
|
use Monolog\Handler\NullHandler;
|
||||||
use Monolog\Handler\StreamHandler;
|
|
||||||
use Monolog\Handler\SyslogUdpHandler;
|
use Monolog\Handler\SyslogUdpHandler;
|
||||||
use Monolog\Processor\PsrLogMessageProcessor;
|
use Monolog\Processor\PsrLogMessageProcessor;
|
||||||
|
|
||||||
|
|
@ -45,7 +44,7 @@ return [
|
||||||
| utilizes the Monolog PHP logging library, which includes a variety
|
| utilizes the Monolog PHP logging library, which includes a variety
|
||||||
| of powerful log handlers and formatters that you're free to use.
|
| 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"
|
| "errorlog", "monolog", "custom", "stack"
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
@ -76,7 +75,7 @@ return [
|
||||||
'slack' => [
|
'slack' => [
|
||||||
'driver' => 'slack',
|
'driver' => 'slack',
|
||||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||||
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
|
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||||
'level' => env('LOG_LEVEL', 'critical'),
|
'level' => env('LOG_LEVEL', 'critical'),
|
||||||
'replace_placeholders' => true,
|
'replace_placeholders' => true,
|
||||||
|
|
@ -97,7 +96,6 @@ return [
|
||||||
'stderr' => [
|
'stderr' => [
|
||||||
'driver' => 'monolog',
|
'driver' => 'monolog',
|
||||||
'level' => env('LOG_LEVEL', 'debug'),
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
'handler' => StreamHandler::class,
|
|
||||||
'handler_with' => [
|
'handler_with' => [
|
||||||
'stream' => 'php://stderr',
|
'stream' => 'php://stderr',
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ return [
|
||||||
|
|
||||||
'from' => [
|
'from' => [
|
||||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||||
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
|
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
67
config/sanctum.php
Normal file
67
config/sanctum.php
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?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,
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -214,20 +214,4 @@ return [
|
||||||
|
|
||||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
'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',
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
18
config/ttwitter.php
Normal file
18
config/ttwitter.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?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;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends Factory<Article>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Article>
|
||||||
*/
|
*/
|
||||||
class ArticleFactory extends Factory
|
class ArticleFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,10 @@
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
use App\Models\Bio;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends Factory<Bio>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Bio>
|
||||||
*/
|
*/
|
||||||
class BioFactory extends Factory
|
class BioFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends Factory<Bookmark>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Bookmark>
|
||||||
*/
|
*/
|
||||||
class BookmarkFactory extends Factory
|
class BookmarkFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends Factory<Contact>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Contact>
|
||||||
*/
|
*/
|
||||||
class ContactFactory extends Factory
|
class ContactFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends Factory<Like>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Like>
|
||||||
*/
|
*/
|
||||||
class LikeFactory extends Factory
|
class LikeFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends Factory<Media>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Media>
|
||||||
*/
|
*/
|
||||||
class MediaFactory extends Factory
|
class MediaFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use App\Models\MicropubClient;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends Factory<MicropubClient>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\MicropubClient>
|
||||||
*/
|
*/
|
||||||
class MicropubClientFactory extends Factory
|
class MicropubClientFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends Factory<Note>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Note>
|
||||||
*/
|
*/
|
||||||
class NoteFactory extends Factory
|
class NoteFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use App\Models\Passkey;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends Factory<Passkey>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Passkey>
|
||||||
*/
|
*/
|
||||||
class PasskeyFactory extends Factory
|
class PasskeyFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use App\Models\Place;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends Factory<Place>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Place>
|
||||||
*/
|
*/
|
||||||
class PlaceFactory extends Factory
|
class PlaceFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,10 @@
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
use App\Models\SyndicationTarget;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends Factory<SyndicationTarget>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\SyndicationTarget>
|
||||||
*/
|
*/
|
||||||
class SyndicationTargetFactory extends Factory
|
class SyndicationTargetFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use App\Models\Tag;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends Factory<Tag>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Tag>
|
||||||
*/
|
*/
|
||||||
class TagFactory extends Factory
|
class TagFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends Factory<User>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||||
*/
|
*/
|
||||||
class UserFactory extends Factory
|
class UserFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use App\Models\WebMention;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends Factory<WebMention>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\WebMention>
|
||||||
*/
|
*/
|
||||||
class WebMentionFactory extends Factory
|
class WebMentionFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,6 @@ namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\Like;
|
use App\Models\Like;
|
||||||
use Faker\Generator;
|
use Faker\Generator;
|
||||||
use Faker\Provider\en_US\Person;
|
|
||||||
use Faker\Provider\Internet;
|
|
||||||
use Faker\Provider\Lorem;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
@ -22,9 +19,9 @@ class LikesTableSeeder extends Seeder
|
||||||
|
|
||||||
$now = Carbon::now()->subDays(rand(3, 6));
|
$now = Carbon::now()->subDays(rand(3, 6));
|
||||||
$faker = new Generator;
|
$faker = new Generator;
|
||||||
$faker->addProvider(new Person($faker));
|
$faker->addProvider(new \Faker\Provider\en_US\Person($faker));
|
||||||
$faker->addProvider(new Lorem($faker));
|
$faker->addProvider(new \Faker\Provider\Lorem($faker));
|
||||||
$faker->addProvider(new Internet($faker));
|
$faker->addProvider(new \Faker\Provider\Internet($faker));
|
||||||
$likeFromAuthor = Like::create([
|
$likeFromAuthor = Like::create([
|
||||||
'url' => $faker->url,
|
'url' => $faker->url,
|
||||||
'author_url' => $faker->url,
|
'author_url' => $faker->url,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
{
|
{
|
||||||
"preset": "laravel"
|
"preset": "laravel",
|
||||||
|
"rules": {
|
||||||
|
"concat_space": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
@section('title')Merge Places « Admin CP « @stop
|
@section('title')Merge Places « Admin CP « @stop
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<p>We shall be merging {{ $first->name }}. It’s location is <code>{{ $first->latitude }}, {{ $first->longitude }}</code>.</p>
|
<p>We shall be merging {{ $first->name }}. It’s location is <code>Point({{ $first->location }})</code>.</p>
|
||||||
<ul>
|
<ul>
|
||||||
@foreach($places as $place)
|
@foreach($places as $place)
|
||||||
<li>
|
<li>
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,15 @@
|
||||||
@foreach($bookmarks as $bookmark)
|
@foreach($bookmarks as $bookmark)
|
||||||
<div class="h-entry">
|
<div class="h-entry">
|
||||||
<div class="bookmark-link">
|
<div class="bookmark-link">
|
||||||
<a class="u-bookmark-of<?php if ($bookmark->name !== null) { echo ' h-cite'; } ?>" href="{{ $bookmark->url }}">
|
<a class="u-bookmark-of<?php if ($bookmark->name !== null) { echo ' h-cite'; } ?>" href="{{ $bookmark->uri }}">
|
||||||
@isset($bookmark->name)
|
@isset($bookmark->name)
|
||||||
{{ $bookmark->name }}
|
{{ $bookmark->name }}
|
||||||
@endisset
|
@endisset
|
||||||
|
|
||||||
@empty($bookmark->name)
|
@empty($bookmark->name)
|
||||||
{{ $bookmark->url }}
|
{{ $bookmark->uri }}
|
||||||
@endempty
|
@endempty
|
||||||
</a> <a href="{{ $bookmark->url }}">🔗</a>
|
</a> <a href="{{ $bookmark->uri }}">🔗</a>
|
||||||
</div>
|
</div>
|
||||||
@isset($bookmark->content)
|
@isset($bookmark->content)
|
||||||
<p>{{ $bookmark->content }}</p>
|
<p>{{ $bookmark->content }}</p>
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,15 @@
|
||||||
@foreach($bookmarks as $bookmark)
|
@foreach($bookmarks as $bookmark)
|
||||||
<div class="h-entry">
|
<div class="h-entry">
|
||||||
<div class="bookmark-link">
|
<div class="bookmark-link">
|
||||||
<a class="u-bookmark-of<?php if ($bookmark->name !== null) { echo ' h-cite'; } ?>" href="{{ $bookmark->url }}">
|
<a class="u-bookmark-of<?php if ($bookmark->name !== null) { echo ' h-cite'; } ?>" href="{{ $bookmark->uri }}">
|
||||||
@isset($bookmark->name)
|
@isset($bookmark->name)
|
||||||
{{ $bookmark->name }}
|
{{ $bookmark->name }}
|
||||||
@endisset
|
@endisset
|
||||||
|
|
||||||
@empty($bookmark->name)
|
@empty($bookmark->name)
|
||||||
{{ $bookmark->url }}
|
{{ $bookmark->uri }}
|
||||||
@endempty
|
@endempty
|
||||||
</a> <a href="{{ $bookmark->url }}">🔗</a>
|
</a> <a href="{{ $bookmark->uri }}">🔗</a>
|
||||||
</div>
|
</div>
|
||||||
@isset($bookmark->content)
|
@isset($bookmark->content)
|
||||||
<p>{{ $bookmark->content }}</p>
|
<p>{{ $bookmark->content }}</p>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<div class="h-entry">
|
<div class="h-entry">
|
||||||
@if ($note->in_reply_to)
|
@if ($note->twitter)
|
||||||
|
{!! $note->twitter->html !!}
|
||||||
|
@elseif ($note->in_reply_to)
|
||||||
<div class="u-in-reply-to h-cite reply-to">
|
<div class="u-in-reply-to h-cite reply-to">
|
||||||
In reply to <a href="{{ $note->in_reply_to }}" class="u-url">{{ $note->in_reply_to }}</a>
|
In reply to <a href="{{ $note->in_reply_to }}" class="u-url">{{ $note->in_reply_to }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ abstract class DuskTestCase extends BaseTestCase
|
||||||
/**
|
/**
|
||||||
* Create the RemoteWebDriver instance.
|
* Create the RemoteWebDriver instance.
|
||||||
*
|
*
|
||||||
* @return RemoteWebDriver
|
* @return \Facebook\WebDriver\Remote\RemoteWebDriver
|
||||||
*/
|
*/
|
||||||
protected function driver()
|
protected function driver()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Tests\Feature\Admin;
|
namespace Tests\Feature\Admin;
|
||||||
|
|
||||||
use App\Models\Note;
|
|
||||||
use App\Models\Place;
|
use App\Models\Place;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
@ -79,62 +78,4 @@ class PlacesTest extends TestCase
|
||||||
'name' => 'The Bridgewater',
|
'name' => 'The Bridgewater',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function merge_index_page_loads(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
// Use specific coordinates to avoid haversine acos overflow with extreme faker values
|
|
||||||
$place = Place::factory()->create(['latitude' => 53.48, 'longitude' => -2.24]);
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->get('/admin/places/'.$place->id.'/merge');
|
|
||||||
$response->assertViewIs('admin.places.merge.index');
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function merge_edit_page_loads(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
$place1 = Place::factory()->create();
|
|
||||||
$place2 = Place::factory()->create();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->get('/admin/places/'.$place1->id.'/merge/'.$place2->id);
|
|
||||||
$response->assertViewIs('admin.places.merge.edit');
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function merge_store_with_delete_one_moves_notes_and_deletes_place1(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
$place1 = Place::factory()->create();
|
|
||||||
$place2 = Place::factory()->create();
|
|
||||||
$note = Note::factory()->create(['place_id' => $place1->id]);
|
|
||||||
|
|
||||||
$this->actingAs($user)->post('/admin/places/merge', [
|
|
||||||
'place1' => $place1->id,
|
|
||||||
'place2' => $place2->id,
|
|
||||||
'delete' => '1',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertDatabaseMissing('places', ['id' => $place1->id]);
|
|
||||||
$this->assertDatabaseHas('notes', ['id' => $note->id, 'place_id' => $place2->id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function merge_store_with_delete_two_moves_notes_and_deletes_place2(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
$place1 = Place::factory()->create();
|
|
||||||
$place2 = Place::factory()->create();
|
|
||||||
$note = Note::factory()->create(['place_id' => $place2->id]);
|
|
||||||
|
|
||||||
$this->actingAs($user)->post('/admin/places/merge', [
|
|
||||||
'place1' => $place1->id,
|
|
||||||
'place2' => $place2->id,
|
|
||||||
'delete' => '2',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertDatabaseMissing('places', ['id' => $place2->id]);
|
|
||||||
$this->assertDatabaseHas('notes', ['id' => $note->id, 'place_id' => $place1->id]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Feature\Admin;
|
|
||||||
|
|
||||||
use App\Models\SyndicationTarget;
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class SyndicationTargetsTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function index_requires_authentication(): void
|
|
||||||
{
|
|
||||||
$response = $this->get('/admin/syndication');
|
|
||||||
$response->assertRedirect();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function index_lists_syndication_targets(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
$target = SyndicationTarget::factory()->create();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->get('/admin/syndication');
|
|
||||||
$response->assertOk();
|
|
||||||
$response->assertSeeText($target->uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function create_page_loads(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->get('/admin/syndication/create');
|
|
||||||
$response->assertOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function store_creates_a_new_syndication_target(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
|
|
||||||
$this->actingAs($user)->post('/admin/syndication', [
|
|
||||||
'uid' => 'https://mastodon.social/users/me',
|
|
||||||
'name' => 'Mastodon',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('syndication_targets', [
|
|
||||||
'uid' => 'https://mastodon.social/users/me',
|
|
||||||
'name' => 'Mastodon',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function store_redirects_to_index_after_creation(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->post('/admin/syndication', [
|
|
||||||
'uid' => 'https://mastodon.social/users/me',
|
|
||||||
'name' => 'Mastodon',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response->assertRedirect('/admin/syndication');
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function edit_page_loads_with_target_data(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
$target = SyndicationTarget::factory()->create(['name' => 'My Mastodon']);
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->get("/admin/syndication/{$target->id}/edit");
|
|
||||||
$response->assertOk();
|
|
||||||
$response->assertSee('value="My Mastodon"', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function update_modifies_syndication_target(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
$target = SyndicationTarget::factory()->create(['name' => 'Old Name']);
|
|
||||||
|
|
||||||
$this->actingAs($user)->put("/admin/syndication/{$target->id}", [
|
|
||||||
'uid' => $target->uid,
|
|
||||||
'name' => 'New Name',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('syndication_targets', [
|
|
||||||
'id' => $target->id,
|
|
||||||
'name' => 'New Name',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function update_redirects_to_index(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
$target = SyndicationTarget::factory()->create();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->put("/admin/syndication/{$target->id}", [
|
|
||||||
'uid' => $target->uid,
|
|
||||||
'name' => 'Updated Name',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response->assertRedirect('/admin/syndication');
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function destroy_deletes_syndication_target(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
$target = SyndicationTarget::factory()->create();
|
|
||||||
|
|
||||||
$this->actingAs($user)->delete("/admin/syndication/{$target->id}");
|
|
||||||
|
|
||||||
$this->assertDatabaseMissing('syndication_targets', ['id' => $target->id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function destroy_redirects_to_index(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
$target = SyndicationTarget::factory()->create();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->delete("/admin/syndication/{$target->id}");
|
|
||||||
|
|
||||||
$response->assertRedirect('/admin/syndication');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,7 +6,6 @@ namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Jobs\ProcessBookmark;
|
use App\Jobs\ProcessBookmark;
|
||||||
use App\Models\Bookmark;
|
use App\Models\Bookmark;
|
||||||
use App\Models\Tag;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Queue;
|
use Illuminate\Support\Facades\Queue;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
|
@ -70,27 +69,6 @@ class BookmarksTest extends TestCase
|
||||||
$this->assertDatabaseHas('bookmarks', ['url' => 'https://example.org/blog-post']);
|
$this->assertDatabaseHas('bookmarks', ['url' => 'https://example.org/blog-post']);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function tagged_bookmarks_page_only_shows_bookmarks_with_that_tag(): void
|
|
||||||
{
|
|
||||||
$tag = Tag::factory()->create(['tag' => 'php']);
|
|
||||||
$tagged = Bookmark::factory()->create();
|
|
||||||
$tagged->tags()->attach($tag);
|
|
||||||
$untagged = Bookmark::factory()->create();
|
|
||||||
|
|
||||||
$response = $this->get('/bookmarks/tagged/php');
|
|
||||||
$response->assertViewIs('bookmarks.tagged');
|
|
||||||
$response->assertSee($tagged->url);
|
|
||||||
$response->assertDontSee($untagged->url);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function bookmark_local_uri_attribute_returns_correct_url(): void
|
|
||||||
{
|
|
||||||
$bookmark = Bookmark::factory()->create();
|
|
||||||
$this->assertEquals(config('app.url').'/bookmarks/'.$bookmark->id, $bookmark->local_uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function when_the_bookmark_is_created_check_necessary_tags_are_also_created(): void
|
public function when_the_bookmark_is_created_check_necessary_tags_are_also_created(): void
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Jobs\ProcessLike;
|
use App\Jobs\ProcessLike;
|
||||||
use App\Models\Like;
|
use App\Models\Like;
|
||||||
|
use Codebird\Codebird;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use GuzzleHttp\Handler\MockHandler;
|
use GuzzleHttp\Handler\MockHandler;
|
||||||
use GuzzleHttp\HandlerStack;
|
use GuzzleHttp\HandlerStack;
|
||||||
|
|
@ -193,6 +194,82 @@ class LikesTest extends TestCase
|
||||||
$this->assertNull(Like::find($id)->author_name);
|
$this->assertNull(Like::find($id)->author_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function like_that_is_a_tweet(): void
|
||||||
|
{
|
||||||
|
$like = new Like;
|
||||||
|
$like->url = 'https://twitter.com/jonnybarnes/status/1050823255123251200';
|
||||||
|
$like->save();
|
||||||
|
$id = $like->id;
|
||||||
|
|
||||||
|
$job = new ProcessLike($like);
|
||||||
|
|
||||||
|
$mock = new MockHandler([
|
||||||
|
new Response(201, [], json_encode([
|
||||||
|
'url' => 'https://twitter.com/likes/id',
|
||||||
|
])),
|
||||||
|
]);
|
||||||
|
$handler = HandlerStack::create($mock);
|
||||||
|
$client = new Client(['handler' => $handler]);
|
||||||
|
$this->app->bind(Client::class, function () use ($client) {
|
||||||
|
return $client;
|
||||||
|
});
|
||||||
|
|
||||||
|
$info = (object) [
|
||||||
|
'author_name' => 'Jonny Barnes',
|
||||||
|
'author_url' => 'https://twitter.com/jonnybarnes',
|
||||||
|
'html' => '<div>HTML of the tweet embed</div>',
|
||||||
|
];
|
||||||
|
$codebirdMock = $this->createPartialMock(Codebird::class, ['__call']);
|
||||||
|
$codebirdMock->method('__call')
|
||||||
|
->with('statuses_oembed', $this->anything())
|
||||||
|
->willReturn($info);
|
||||||
|
$this->app->instance(Codebird::class, $codebirdMock);
|
||||||
|
|
||||||
|
$authorship = new Authorship;
|
||||||
|
|
||||||
|
$job->handle($client, $authorship);
|
||||||
|
|
||||||
|
$this->assertEquals('Jonny Barnes', Like::find($id)->author_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function no_error_for_failure_to_posse_with_bridgy(): void
|
||||||
|
{
|
||||||
|
$like = new Like;
|
||||||
|
$like->url = 'https://twitter.com/jonnybarnes/status/1050823255123251200';
|
||||||
|
$like->save();
|
||||||
|
$id = $like->id;
|
||||||
|
|
||||||
|
$job = new ProcessLike($like);
|
||||||
|
|
||||||
|
$mock = new MockHandler([
|
||||||
|
new Response(404, [], 'Not found'),
|
||||||
|
]);
|
||||||
|
$handler = HandlerStack::create($mock);
|
||||||
|
$client = new Client(['handler' => $handler]);
|
||||||
|
$this->app->bind(Client::class, function () use ($client) {
|
||||||
|
return $client;
|
||||||
|
});
|
||||||
|
|
||||||
|
$info = (object) [
|
||||||
|
'author_name' => 'Jonny Barnes',
|
||||||
|
'author_url' => 'https://twitter.com/jonnybarnes',
|
||||||
|
'html' => '<div>HTML of the tweet embed</div>',
|
||||||
|
];
|
||||||
|
$codebirdMock = $this->createPartialMock(Codebird::class, ['__call']);
|
||||||
|
$codebirdMock->method('__call')
|
||||||
|
->with('statuses_oembed', $this->anything())
|
||||||
|
->willReturn($info);
|
||||||
|
$this->app->instance(Codebird::class, $codebirdMock);
|
||||||
|
|
||||||
|
$authorship = new Authorship;
|
||||||
|
|
||||||
|
$job->handle($client, $authorship);
|
||||||
|
|
||||||
|
$this->assertEquals('Jonny Barnes', Like::find($id)->author_name);
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function unknown_like_gives_not_found_response(): void
|
public function unknown_like_gives_not_found_response(): void
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -820,7 +820,7 @@ class MicropubControllerTest extends TestCase
|
||||||
public function micropub_client_api_request_can_delete_photo(): void
|
public function micropub_client_api_request_can_delete_photo(): void
|
||||||
{
|
{
|
||||||
$note = Note::factory()->create();
|
$note = Note::factory()->create();
|
||||||
$media = new Media;
|
$media = new \App\Models\Media;
|
||||||
$media->path = 'https://example.org/photo.jpg';
|
$media->path = 'https://example.org/photo.jpg';
|
||||||
$media->type = 'image';
|
$media->type = 'image';
|
||||||
$media->save();
|
$media->save();
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ namespace Tests\Unit\Jobs;
|
||||||
use App\Jobs\SaveScreenshot;
|
use App\Jobs\SaveScreenshot;
|
||||||
use App\Models\Bookmark;
|
use App\Models\Bookmark;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use GuzzleHttp\Exception\ConnectException;
|
|
||||||
use GuzzleHttp\Handler\MockHandler;
|
use GuzzleHttp\Handler\MockHandler;
|
||||||
use GuzzleHttp\HandlerStack;
|
use GuzzleHttp\HandlerStack;
|
||||||
use GuzzleHttp\Middleware;
|
use GuzzleHttp\Middleware;
|
||||||
|
|
@ -46,7 +45,7 @@ class SaveScreenshotJobTest extends TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry connection exceptions
|
// Retry connection exceptions
|
||||||
if ($exception instanceof ConnectException) {
|
if ($exception instanceof \GuzzleHttp\Exception\ConnectException) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,7 +115,7 @@ class SaveScreenshotJobTest extends TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry connection exceptions
|
// Retry connection exceptions
|
||||||
if ($exception instanceof ConnectException) {
|
if ($exception instanceof \GuzzleHttp\Exception\ConnectException) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ namespace Tests\Unit;
|
||||||
use App\Models\Media;
|
use App\Models\Media;
|
||||||
use App\Models\Note;
|
use App\Models\Note;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use PHPUnit\Framework\Attributes\DataProvider;
|
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
|
@ -39,68 +38,4 @@ class MediaTest extends TestCase
|
||||||
|
|
||||||
$this->assertEquals($absoluteUrl, $media->url);
|
$this->assertEquals($absoluteUrl, $media->url);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function local_paths_get_storage_url_prepended(): void
|
|
||||||
{
|
|
||||||
$media = new Media;
|
|
||||||
$media->path = 'photo.jpg';
|
|
||||||
|
|
||||||
$this->assertEquals(config('app.url').'/storage/photo.jpg', $media->url);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function medium_url_returns_resized_path(): void
|
|
||||||
{
|
|
||||||
$media = new Media;
|
|
||||||
$media->path = 'photo.jpg';
|
|
||||||
|
|
||||||
$this->assertEquals(config('app.url').'/storage/photo-medium.jpg', $media->mediumurl);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function small_url_returns_resized_path(): void
|
|
||||||
{
|
|
||||||
$media = new Media;
|
|
||||||
$media->path = 'photo.jpg';
|
|
||||||
|
|
||||||
$this->assertEquals(config('app.url').'/storage/photo-small.jpg', $media->smallurl);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function size_url_handles_dotted_basename(): void
|
|
||||||
{
|
|
||||||
$media = new Media;
|
|
||||||
$media->path = 'file.name.png';
|
|
||||||
|
|
||||||
$this->assertEquals(config('app.url').'/storage/file.name-medium.png', $media->mediumurl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider mimeTypeProvider
|
|
||||||
*/
|
|
||||||
#[DataProvider('mimeTypeProvider')]
|
|
||||||
public function mimetype_returns_correct_mime_for_extension(string $path, string $expected): void
|
|
||||||
{
|
|
||||||
$media = new Media;
|
|
||||||
$media->path = $path;
|
|
||||||
|
|
||||||
$this->assertEquals($expected, $media->mimetype);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function mimeTypeProvider(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
['photo.gif', 'image/gif'],
|
|
||||||
['photo.jpeg', 'image/jpeg'],
|
|
||||||
['photo.jpg', 'image/jpeg'],
|
|
||||||
['photo.png', 'image/png'],
|
|
||||||
['photo.svg', 'image/svg+xml'],
|
|
||||||
['photo.tiff', 'image/tiff'],
|
|
||||||
['photo.webp', 'image/webp'],
|
|
||||||
['video.mp4', 'video/mp4'],
|
|
||||||
['video.mkv', 'video/mkv'],
|
|
||||||
['file.bin', 'application/octet-stream'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Unit;
|
|
||||||
|
|
||||||
use App\Services\Micropub\Data\UpdateData;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use JsonException;
|
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class MicropubUpdateDataTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @throws JsonException
|
|
||||||
*/
|
|
||||||
#[Test]
|
|
||||||
public function from_request_parses_json_body(): void
|
|
||||||
{
|
|
||||||
$request = Request::create('/micropub', 'POST', [], [], [], [
|
|
||||||
'CONTENT_TYPE' => 'application/json',
|
|
||||||
], json_encode([
|
|
||||||
'token_data' => ['me' => 'https://example.com'],
|
|
||||||
'url' => 'https://example.com/notes/1',
|
|
||||||
'replace' => ['content' => ['new content']],
|
|
||||||
'add' => ['syndication' => ['https://twitter.com']],
|
|
||||||
'delete' => ['category' => ['old-tag']],
|
|
||||||
], JSON_THROW_ON_ERROR));
|
|
||||||
|
|
||||||
$dto = UpdateData::fromRequest($request);
|
|
||||||
|
|
||||||
$this->assertEquals(['me' => 'https://example.com'], $dto->tokenData);
|
|
||||||
$this->assertEquals('https://example.com/notes/1', $dto->updateUrl);
|
|
||||||
$this->assertEquals(['content' => ['new content']], $dto->updateReplace);
|
|
||||||
$this->assertEquals(['syndication' => ['https://twitter.com']], $dto->updateAdd);
|
|
||||||
$this->assertEquals(['category' => ['old-tag']], $dto->updateDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function from_request_parses_form_encoded_body(): void
|
|
||||||
{
|
|
||||||
$request = Request::create('/micropub', 'POST', [
|
|
||||||
'token_data' => ['me' => 'https://example.com'],
|
|
||||||
'url' => 'https://example.com/notes/1',
|
|
||||||
'replace' => ['content' => ['new content']],
|
|
||||||
'add' => ['syndication' => ['https://twitter.com']],
|
|
||||||
'delete' => ['category' => ['old-tag']],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$dto = UpdateData::fromRequest($request);
|
|
||||||
|
|
||||||
$this->assertEquals(['me' => 'https://example.com'], $dto->tokenData);
|
|
||||||
$this->assertEquals('https://example.com/notes/1', $dto->updateUrl);
|
|
||||||
$this->assertEquals(['content' => ['new content']], $dto->updateReplace);
|
|
||||||
$this->assertEquals(['syndication' => ['https://twitter.com']], $dto->updateAdd);
|
|
||||||
$this->assertEquals(['category' => ['old-tag']], $dto->updateDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws JsonException
|
|
||||||
*/
|
|
||||||
#[Test]
|
|
||||||
public function from_request_nullable_fields_default_to_null(): void
|
|
||||||
{
|
|
||||||
$request = Request::create('/micropub', 'POST', [], [], [], [
|
|
||||||
'CONTENT_TYPE' => 'application/json',
|
|
||||||
], json_encode([
|
|
||||||
'token_data' => ['me' => 'https://example.com'],
|
|
||||||
'url' => 'https://example.com/notes/1',
|
|
||||||
], JSON_THROW_ON_ERROR));
|
|
||||||
|
|
||||||
$dto = UpdateData::fromRequest($request);
|
|
||||||
|
|
||||||
$this->assertNull($dto->updateReplace);
|
|
||||||
$this->assertNull($dto->updateAdd);
|
|
||||||
$this->assertNull($dto->updateDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function from_array_maps_all_fields(): void
|
|
||||||
{
|
|
||||||
$dto = UpdateData::fromArray([
|
|
||||||
'token_data' => ['me' => 'https://example.com'],
|
|
||||||
'update_url' => 'https://example.com/notes/1',
|
|
||||||
'update_replace' => ['content' => ['updated']],
|
|
||||||
'update_add' => ['category' => ['new-tag']],
|
|
||||||
'update_delete' => ['syndication' => ['https://old.example.com']],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertEquals(['me' => 'https://example.com'], $dto->tokenData);
|
|
||||||
$this->assertEquals('https://example.com/notes/1', $dto->updateUrl);
|
|
||||||
$this->assertEquals(['content' => ['updated']], $dto->updateReplace);
|
|
||||||
$this->assertEquals(['category' => ['new-tag']], $dto->updateAdd);
|
|
||||||
$this->assertEquals(['syndication' => ['https://old.example.com']], $dto->updateDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function from_array_nullable_fields_default_to_null(): void
|
|
||||||
{
|
|
||||||
$dto = UpdateData::fromArray([
|
|
||||||
'token_data' => ['me' => 'https://example.com'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertNull($dto->updateUrl);
|
|
||||||
$this->assertNull($dto->updateReplace);
|
|
||||||
$this->assertNull($dto->updateAdd);
|
|
||||||
$this->assertNull($dto->updateDelete);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -383,6 +383,24 @@ class NotesTest extends TestCase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For now, just reply on a cached object instead of actually querying Twitter.
|
||||||
|
*/
|
||||||
|
#[Test]
|
||||||
|
public function check_in_reply_to_is_twitter_link(): void
|
||||||
|
{
|
||||||
|
$tempContent = (object) [
|
||||||
|
'html' => 'something random',
|
||||||
|
];
|
||||||
|
Cache::put('933662564587855877', $tempContent);
|
||||||
|
|
||||||
|
$note = Note::factory()->create([
|
||||||
|
'in_reply_to' => 'https://twitter.com/someRando/status/933662564587855877',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals($tempContent, $note->twitter);
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function latitude_and_longitude_can_be_parsed_from_plain_location(): void
|
public function latitude_and_longitude_can_be_parsed_from_plain_location(): void
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@ namespace Tests\Unit;
|
||||||
|
|
||||||
use App\Models\Note;
|
use App\Models\Note;
|
||||||
use App\Models\WebMention;
|
use App\Models\WebMention;
|
||||||
|
use Codebird\Codebird;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
|
@ -80,6 +82,41 @@ class WebMentionTest extends TestCase
|
||||||
$this->assertEquals($expected, $webmention->createPhotoLink($twitterProfileImage));
|
$this->assertEquals($expected, $webmention->createPhotoLink($twitterProfileImage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function create_photo_link_returns_cached_twitter_photo_links(): void
|
||||||
|
{
|
||||||
|
$webmention = new WebMention;
|
||||||
|
$twitterURL = 'https://twitter.com/example';
|
||||||
|
$expected = 'https://pbs.twimg.com/static_profile_link.jpg';
|
||||||
|
Cache::put($twitterURL, $expected, 1);
|
||||||
|
$this->assertEquals($expected, $webmention->createPhotoLink($twitterURL));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function create_photo_link_resolves_twitter_photo_links(): void
|
||||||
|
{
|
||||||
|
$info = (object) [
|
||||||
|
'profile_image_url_https' => 'https://pbs.twimg.com/static_profile_link.jpg',
|
||||||
|
];
|
||||||
|
$codebirdMock = $this->createPartialMock(Codebird::class, ['__call']);
|
||||||
|
$codebirdMock->method('__call')
|
||||||
|
->with('users_show', $this->anything())
|
||||||
|
->willReturn($info);
|
||||||
|
$this->app->instance(Codebird::class, $codebirdMock);
|
||||||
|
|
||||||
|
Cache::shouldReceive('has')
|
||||||
|
->once()
|
||||||
|
->andReturn(false);
|
||||||
|
Cache::shouldReceive('put')
|
||||||
|
->once()
|
||||||
|
->andReturn(true);
|
||||||
|
|
||||||
|
$webmention = new WebMention;
|
||||||
|
$twitterURL = 'https://twitter.com/example';
|
||||||
|
$expected = 'https://pbs.twimg.com/static_profile_link.jpg';
|
||||||
|
$this->assertEquals($expected, $webmention->createPhotoLink($twitterURL));
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function get_reply_attribute_defaults_to_null(): void
|
public function get_reply_attribute_defaults_to_null(): void
|
||||||
{
|
{
|
||||||
|
|
@ -94,37 +131,4 @@ class WebMentionTest extends TestCase
|
||||||
$webmention->mf2 = json_encode(['no_html' => 'found_here']);
|
$webmention->mf2 = json_encode(['no_html' => 'found_here']);
|
||||||
$this->assertNull($webmention->reply);
|
$this->assertNull($webmention->reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function author_attribute_returns_null_when_mf2_is_null(): void
|
|
||||||
{
|
|
||||||
$webmention = new WebMention;
|
|
||||||
$webmention->mf2 = null;
|
|
||||||
$this->assertNull($webmention->author);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function author_attribute_returns_hcard_without_photo_key_when_no_photo_present(): void
|
|
||||||
{
|
|
||||||
$webmention = new WebMention;
|
|
||||||
$webmention->mf2 = json_encode([
|
|
||||||
'items' => [[
|
|
||||||
'type' => ['h-entry'],
|
|
||||||
'properties' => [
|
|
||||||
'author' => [[
|
|
||||||
'type' => ['h-card'],
|
|
||||||
'properties' => [
|
|
||||||
'name' => ['Test Person'],
|
|
||||||
'url' => ['https://example.com'],
|
|
||||||
],
|
|
||||||
]],
|
|
||||||
'content' => ['hello'],
|
|
||||||
],
|
|
||||||
]],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$hCard = $webmention->author;
|
|
||||||
$this->assertNotNull($hCard);
|
|
||||||
$this->assertArrayNotHasKey('photo', $hCard['properties'] ?? []);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue