Merge branch 'release/0.5.17'
This commit is contained in:
commit
aa126d728d
44 changed files with 4879 additions and 4500 deletions
|
|
@ -14,6 +14,7 @@ addons:
|
|||
packages:
|
||||
- nginx
|
||||
- realpath
|
||||
- postgresql-9.6-postgis-2.3
|
||||
artifacts:
|
||||
s3_region: "eu-west-1"
|
||||
paths:
|
||||
|
|
|
|||
|
|
@ -62,29 +62,29 @@ class MicropubController extends Controller
|
|||
// Log the request
|
||||
Log::debug($request);
|
||||
if ($tokenData->hasClaim('scope')) {
|
||||
if (($request->input('h') == 'entry') || ($request->input('type')[0] == 'h-entry')) {
|
||||
if (($request->input('h') == 'entry') || ($request->input('type.0') == 'h-entry')) {
|
||||
if (stristr($tokenData->getClaim('scope'), 'create') === false) {
|
||||
return $this->returnInsufficientScopeResponse();
|
||||
}
|
||||
$data = [];
|
||||
$data['client-id'] = $tokenData->getClaim('client_id');
|
||||
if ($request->header('Content-Type') == 'application/json') {
|
||||
if (is_string($request->input('properties.content')[0])) {
|
||||
$data['content'] = $request->input('properties.content')[0]; //plaintext content
|
||||
if (is_string($request->input('properties.content.0'))) {
|
||||
$data['content'] = $request->input('properties.content.0'); //plaintext content
|
||||
}
|
||||
if (is_array($request->input('properties.content')[0])
|
||||
&& array_key_exists('html', $request->input('properties.content')[0])
|
||||
if (is_array($request->input('properties.content.0'))
|
||||
&& array_key_exists('html', $request->input('properties.content.0'))
|
||||
) {
|
||||
$data['content'] = $request->input('properties.content')[0]['html'];
|
||||
$data['content'] = $request->input('properties.content.0.html');
|
||||
}
|
||||
$data['in-reply-to'] = $request->input('properties.in-reply-to')[0];
|
||||
$data['in-reply-to'] = $request->input('properties.in-reply-to.0');
|
||||
// check location is geo: string
|
||||
if (is_string($request->input('properties.location.0'))) {
|
||||
$data['location'] = $request->input('properties.location.0');
|
||||
}
|
||||
// check location is h-card
|
||||
if (is_array($request->input('properties.location.0'))) {
|
||||
if ($request->input('properties.location.0.type' === 'h-card')) {
|
||||
if ($request->input('properties.location.0.type.0' === 'h-card')) {
|
||||
try {
|
||||
$place = $this->placeService->createPlaceFromCheckin($request->input('properties.location.0'));
|
||||
$data['checkin'] = $place->longurl;
|
||||
|
|
@ -93,7 +93,7 @@ class MicropubController extends Controller
|
|||
}
|
||||
}
|
||||
}
|
||||
$data['published'] = $request->input('properties.published')[0];
|
||||
$data['published'] = $request->input('properties.published.0');
|
||||
//create checkin place
|
||||
if (array_key_exists('checkin', $request->input('properties'))) {
|
||||
$data['swarm-url'] = $request->input('properties.syndication.0');
|
||||
|
|
@ -374,7 +374,7 @@ class MicropubController extends Controller
|
|||
|
||||
//check post scope
|
||||
if ($tokenData->hasClaim('scope')) {
|
||||
if (stristr($token->getClaim('scope'), 'post') === false) {
|
||||
if (stristr($tokenData->getClaim('scope'), 'create') === false) {
|
||||
return $this->returnInsufficientScopeResponse();
|
||||
}
|
||||
//check media valid
|
||||
|
|
|
|||
|
|
@ -2,16 +2,9 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Twitter;
|
||||
use HTMLPurifier;
|
||||
use App\{Note, Tag};
|
||||
use GuzzleHttp\Client;
|
||||
use HTMLPurifier_Config;
|
||||
use App\Note;
|
||||
use Illuminate\Http\Request;
|
||||
use Jonnybarnes\IndieWeb\Numbers;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Jonnybarnes\WebmentionsParser\Authorship;
|
||||
|
||||
// Need to sort out Twitter and webmentions!
|
||||
|
||||
|
|
@ -25,41 +18,13 @@ class NotesController extends Controller
|
|||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$notes = Note::orderBy('id', 'desc')->with('webmentions', 'place', 'media')->paginate(10);
|
||||
foreach ($notes as $note) {
|
||||
$replies = 0;
|
||||
foreach ($note->webmentions as $webmention) {
|
||||
if ($webmention->type == 'in-reply-to') {
|
||||
$replies++;
|
||||
}
|
||||
}
|
||||
$note->replies = $replies;
|
||||
$note->twitter = $this->checkTwitterReply($note->in_reply_to);
|
||||
$note->iso8601_time = $note->updated_at->toISO8601String();
|
||||
$note->human_time = $note->updated_at->diffForHumans();
|
||||
if ($note->location && ($note->place === null)) {
|
||||
$pieces = explode(':', $note->location);
|
||||
$latlng = explode(',', $pieces[0]);
|
||||
$note->latitude = trim($latlng[0]);
|
||||
$note->longitude = trim($latlng[1]);
|
||||
$note->address = $this->reverseGeoCode((float) trim($latlng[0]), (float) trim($latlng[1]));
|
||||
}
|
||||
if ($note->place !== null) {
|
||||
$lnglat = explode(' ', $note->place->location);
|
||||
$note->latitude = $lnglat[1];
|
||||
$note->longitude = $lnglat[0];
|
||||
$note->address = $note->place->name;
|
||||
$note->placeLink = '/places/' . $note->place->slug;
|
||||
}
|
||||
/*$mediaLinks = [];
|
||||
foreach ($note->media()->get() as $media) {
|
||||
$mediaLinks[] = $media->url;
|
||||
}*/
|
||||
}
|
||||
$notes = Note::orderBy('id', 'desc')
|
||||
->with('place', 'media', 'client')
|
||||
->withCount(['webmentions As replies' => function ($query) {
|
||||
$query->where('type', 'in-reply-to');
|
||||
}])->paginate(10);
|
||||
|
||||
$homepage = ($request->path() == '/');
|
||||
|
||||
return view('notes.index', compact('notes', 'homepage'));
|
||||
return view('notes.index', compact('notes'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -70,90 +35,9 @@ class NotesController extends Controller
|
|||
*/
|
||||
public function show($urlId)
|
||||
{
|
||||
$numbers = new Numbers();
|
||||
$authorship = new Authorship();
|
||||
$realId = $numbers->b60tonum($urlId);
|
||||
$note = Note::find($realId);
|
||||
$replies = [];
|
||||
$reposts = [];
|
||||
$likes = [];
|
||||
$carbon = new \Carbon\Carbon();
|
||||
foreach ($note->webmentions as $webmention) {
|
||||
/*
|
||||
reply->url |
|
||||
reply->photo | Author
|
||||
reply->name |
|
||||
reply->source
|
||||
reply->date
|
||||
reply->reply
|
||||
$note = Note::nb60($urlId)->with('webmentions')->first();
|
||||
|
||||
repost->url |
|
||||
repost->photo | Author
|
||||
repost->name |
|
||||
repost->date
|
||||
repost->source
|
||||
|
||||
like->url |
|
||||
like->photo | Author
|
||||
like->name |
|
||||
*/
|
||||
$microformats = json_decode($webmention->mf2, true);
|
||||
$authorHCard = $authorship->findAuthor($microformats);
|
||||
$content['url'] = $authorHCard['properties']['url'][0];
|
||||
$content['photo'] = $this->createPhotoLink($authorHCard['properties']['photo'][0]);
|
||||
$content['name'] = $authorHCard['properties']['name'][0];
|
||||
switch ($webmention->type) {
|
||||
case 'in-reply-to':
|
||||
$content['source'] = $webmention->source;
|
||||
if (isset($microformats['items'][0]['properties']['published'][0])) {
|
||||
try {
|
||||
$content['date'] = $carbon->parse(
|
||||
$microformats['items'][0]['properties']['published'][0]
|
||||
)->toDayDateTimeString();
|
||||
} catch (\Exception $exception) {
|
||||
$content['date'] = $webmention->updated_at->toDayDateTimeString();
|
||||
}
|
||||
} else {
|
||||
$content['date'] = $webmention->updated_at->toDayDateTimeString();
|
||||
}
|
||||
$content['reply'] = $this->filterHTML(
|
||||
$microformats['items'][0]['properties']['content'][0]['html']
|
||||
);
|
||||
$replies[] = $content;
|
||||
break;
|
||||
|
||||
case 'repost-of':
|
||||
$content['date'] = $carbon->parse(
|
||||
$microformats['items'][0]['properties']['published'][0]
|
||||
)->toDayDateTimeString();
|
||||
$content['source'] = $webmention->source;
|
||||
$reposts[] = $content;
|
||||
break;
|
||||
|
||||
case 'like-of':
|
||||
$likes[] = $content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$note->twitter = $this->checkTwitterReply($note->in_reply_to);
|
||||
$note->iso8601_time = $note->updated_at->toISO8601String();
|
||||
$note->human_time = $note->updated_at->diffForHumans();
|
||||
if ($note->location && ($note->place === null)) {
|
||||
$pieces = explode(':', $note->location);
|
||||
$latlng = explode(',', $pieces[0]);
|
||||
$note->latitude = trim($latlng[0]);
|
||||
$note->longitude = trim($latlng[1]);
|
||||
$note->address = $this->reverseGeoCode((float) trim($latlng[0]), (float) trim($latlng[1]));
|
||||
}
|
||||
if ($note->place !== null) {
|
||||
$lnglat = explode(' ', $note->place->location);
|
||||
$note->latitude = $lnglat[1];
|
||||
$note->longitude = $lnglat[0];
|
||||
$note->address = $note->place->name;
|
||||
$note->placeLink = '/places/' . $note->place->slug;
|
||||
}
|
||||
|
||||
return view('notes.show', compact('note', 'replies', 'reposts', 'likes'));
|
||||
return view('notes.show', compact('note'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -164,12 +48,7 @@ class NotesController extends Controller
|
|||
*/
|
||||
public function redirect($decId)
|
||||
{
|
||||
$numbers = new Numbers();
|
||||
$realId = $numbers->numto60($decId);
|
||||
|
||||
$url = config('app.url') . '/notes/' . $realId;
|
||||
|
||||
return redirect($url);
|
||||
return redirect(config('app.url') . '/notes/' . (new Numbers())->numto60($decId));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -183,180 +62,7 @@ class NotesController extends Controller
|
|||
$notes = Note::whereHas('tags', function ($query) use ($tag) {
|
||||
$query->where('tag', $tag);
|
||||
})->get();
|
||||
foreach ($notes as $note) {
|
||||
$note->iso8601_time = $note->updated_at->toISO8601String();
|
||||
$note->human_time = $note->updated_at->diffForHumans();
|
||||
}
|
||||
|
||||
return view('notes.tagged', compact('notes', 'tag'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the photo link.
|
||||
*
|
||||
* We shall leave twitter.com and twimg.com links as they are. Then we shall
|
||||
* check for local copies, if that fails leave the link as is.
|
||||
*
|
||||
* @param string
|
||||
* @return string
|
||||
*/
|
||||
public function createPhotoLink($url)
|
||||
{
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
if ($host == 'pbs.twimg.com') {
|
||||
//make sure we use HTTPS, we know twitter supports it
|
||||
return str_replace('http://', 'https://', $url);
|
||||
}
|
||||
if ($host == 'twitter.com') {
|
||||
if (Cache::has($url)) {
|
||||
return Cache::get($url);
|
||||
}
|
||||
$username = parse_url($url, PHP_URL_PATH);
|
||||
try {
|
||||
$info = Twitter::getUsers(['screen_name' => $username]);
|
||||
$profile_image = $info->profile_image_url_https;
|
||||
Cache::put($url, $profile_image, 10080); //1 week
|
||||
} catch (Exception $e) {
|
||||
return $url; //not sure here
|
||||
}
|
||||
|
||||
return $profile_image;
|
||||
}
|
||||
$filesystem = new Filesystem();
|
||||
if ($filesystem->exists(public_path() . '/assets/profile-images/' . $host . '/image')) {
|
||||
return '/assets/profile-images/' . $host . '/image';
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Twitter!!!
|
||||
*
|
||||
* @param string The reply to URL
|
||||
* @return string | null
|
||||
*/
|
||||
private function checkTwitterReply($url)
|
||||
{
|
||||
if ($url == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mb_substr($url, 0, 20, 'UTF-8') !== 'https://twitter.com/') {
|
||||
return;
|
||||
}
|
||||
|
||||
$arr = explode('/', $url);
|
||||
$tweetId = end($arr);
|
||||
if (Cache::has($tweetId)) {
|
||||
return Cache::get($tweetId);
|
||||
}
|
||||
try {
|
||||
$oEmbed = Twitter::getOembed([
|
||||
'id' => $tweetId,
|
||||
'align' => 'center',
|
||||
'omit_script' => true,
|
||||
'maxwidth' => 550,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
}
|
||||
Cache::put($tweetId, $oEmbed, ($oEmbed->cache_age / 60));
|
||||
|
||||
return $oEmbed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the HTML in a reply webmention.
|
||||
*
|
||||
* @param string The reply HTML
|
||||
* @return string The filtered HTML
|
||||
*/
|
||||
private function filterHTML($html)
|
||||
{
|
||||
$config = HTMLPurifier_Config::createDefault();
|
||||
$config->set('Cache.SerializerPath', storage_path() . '/HTMLPurifier');
|
||||
$config->set('HTML.TargetBlank', true);
|
||||
$purifier = new HTMLPurifier($config);
|
||||
|
||||
return $purifier->purify($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a reverse geocode lookup of a `lat,lng` value.
|
||||
*
|
||||
* @param float The latitude
|
||||
* @param float The longitude
|
||||
* @return string The location HTML
|
||||
*/
|
||||
public function reverseGeoCode(float $latitude, float $longitude): string
|
||||
{
|
||||
$latlng = $latitude . ',' . $longitude;
|
||||
|
||||
return Cache::get($latlng, function () use ($latlng, $latitude, $longitude) {
|
||||
$guzzle = new Client();
|
||||
$response = $guzzle->request('GET', 'https://nominatim.openstreetmap.org/reverse', [
|
||||
'query' => [
|
||||
'format' => 'json',
|
||||
'lat' => $latitude,
|
||||
'lon' => $longitude,
|
||||
'zoom' => 18,
|
||||
'addressdetails' => 1,
|
||||
],
|
||||
'headers' => ['User-Agent' => 'jonnybarnes.uk via Guzzle, email jonny@jonnybarnes.uk'],
|
||||
]);
|
||||
$json = json_decode($response->getBody());
|
||||
if (isset($json->address->town)) {
|
||||
$address = '<span class="p-locality">'
|
||||
. $json->address->town
|
||||
. '</span>, <span class="p-country-name">'
|
||||
. $json->address->country
|
||||
. '</span>';
|
||||
Cache::forever($latlng, $address);
|
||||
|
||||
return $address;
|
||||
}
|
||||
if (isset($json->address->city)) {
|
||||
$address = $json->address->city . ', ' . $json->address->country;
|
||||
Cache::forever($latlng, $address);
|
||||
|
||||
return $address;
|
||||
}
|
||||
if (isset($json->address->county)) {
|
||||
$address = '<span class="p-region">'
|
||||
. $json->address->county
|
||||
. '</span>, <span class="p-country-name">'
|
||||
. $json->address->country
|
||||
. '</span>';
|
||||
Cache::forever($latlng, $address);
|
||||
|
||||
return $address;
|
||||
}
|
||||
$adress = '<span class="p-country-name">' . $json->address->country . '</span>';
|
||||
Cache::forever($latlng, $address);
|
||||
|
||||
return $address;
|
||||
});
|
||||
}
|
||||
|
||||
private function getGeoJson($longitude, $latitude, $title, $icon)
|
||||
{
|
||||
$icon = $icon ?? 'marker';
|
||||
|
||||
return
|
||||
"{
|
||||
'type': 'FeatureCollection',
|
||||
'features': [{
|
||||
'type': 'Feature',
|
||||
'geometry': {
|
||||
'type': 'Point',
|
||||
'coordinates': [$longitude, $latitude]
|
||||
},
|
||||
'properties': {
|
||||
'title': '$title',
|
||||
'icon': '$icon'
|
||||
}
|
||||
}]
|
||||
}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
42
app/Jobs/AddClientToDatabase.php
Normal file
42
app/Jobs/AddClientToDatabase.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\MicropubClient;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class AddClientToDatabase implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $client_id;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(string $client_id)
|
||||
{
|
||||
$this->client_id = $client_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (MicropubClient::where('client_url', $this->client_id)->count() == 0) {
|
||||
$client = MicropubClient::create([
|
||||
'client_url' => $this->client_id,
|
||||
'client_name' => $this->client_id, // default client name is the URL
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,16 +3,14 @@
|
|||
namespace App\Jobs;
|
||||
|
||||
use Mf2;
|
||||
use App\Note;
|
||||
use App\WebMention;
|
||||
use GuzzleHttp\Client;
|
||||
use App\{Note, WebMention};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Jonnybarnes\WebmentionsParser\Parser;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use App\Exceptions\RemoteContentNotFoundException;
|
||||
use Illuminate\Queue\{InteractsWithQueue, SerializesModels};
|
||||
|
||||
class ProcessWebMention implements ShouldQueue
|
||||
{
|
||||
|
|
|
|||
|
|
@ -19,4 +19,14 @@ class MicropubClient extends Model
|
|||
* @var array
|
||||
*/
|
||||
protected $fillable = ['client_url', 'client_name'];
|
||||
|
||||
/**
|
||||
* Define the relationship with notes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function notes()
|
||||
{
|
||||
return $this->hasMany('App\Note', 'client_id', 'client_url');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
182
app/Note.php
182
app/Note.php
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use Cache;
|
||||
use Twitter;
|
||||
use Normalizer;
|
||||
use GuzzleHttp\Client;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Jonnybarnes\IndieWeb\Numbers;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
|
@ -33,6 +36,16 @@ class Note extends Model
|
|||
return $this->belongsToMany('App\Tag');
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the relationship with clients.
|
||||
*
|
||||
* @var array?
|
||||
*/
|
||||
public function client()
|
||||
{
|
||||
return $this->belongsTo('App\MicropubClient', 'client_id', 'client_url');
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the relationship with webmentions.
|
||||
*
|
||||
|
|
@ -96,17 +109,6 @@ class Note extends Model
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* A mutator to ensure that in-reply-to is always non-empty or null.
|
||||
*
|
||||
* @param string value
|
||||
* @return string
|
||||
*/
|
||||
public function setInReplyToAttribute($value)
|
||||
{
|
||||
$this->attributes['in_reply_to'] = empty($value) ? null : $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the note to Unicode FORM C.
|
||||
*
|
||||
|
|
@ -168,6 +170,26 @@ class Note extends Model
|
|||
return config('app.shorturl') . '/notes/' . $this->nb60id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ISO8601 value for mf2.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getIso8601Attribute()
|
||||
{
|
||||
return $this->updated_at->toISO8601String();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ISO8601 value for mf2.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHumandiffAttribute()
|
||||
{
|
||||
return $this->updated_at->diffForHumans();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pubdate value for RSS feeds.
|
||||
*
|
||||
|
|
@ -179,26 +201,85 @@ class Note extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the relavent client name assocaited with the client id.
|
||||
* Get the latitude value.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getClientNameAttribute()
|
||||
public function getLatitudeAttribute()
|
||||
{
|
||||
if ($this->client_id == null) {
|
||||
if ($this->place !== null) {
|
||||
$lnglat = explode(' ', $this->place->location);
|
||||
|
||||
return $lnglat[1];
|
||||
}
|
||||
if ($this->location !== null) {
|
||||
$pieces = explode(':', $this->location);
|
||||
$latlng = explode(',', $pieces[0]);
|
||||
|
||||
return trim($latlng[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the longitude value.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLongitudeAttribute()
|
||||
{
|
||||
if ($this->place !== null) {
|
||||
$lnglat = explode(' ', $this->place->location);
|
||||
|
||||
return $lnglat[1];
|
||||
}
|
||||
if ($this->location !== null) {
|
||||
$pieces = explode(':', $this->location);
|
||||
$latlng = explode(',', $pieces[0]);
|
||||
|
||||
return trim($latlng[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the address for a note. This is either a reverse geo-code from the
|
||||
* location, or is derived from the associated place.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAddressAttribute()
|
||||
{
|
||||
if ($this->place !== null) {
|
||||
return $this->place->name;
|
||||
}
|
||||
if ($this->location !== null) {
|
||||
return $this->reverseGeoCode((float) $this->latitude, (float) $this->longitude);
|
||||
}
|
||||
}
|
||||
|
||||
public function getTwitterAttribute()
|
||||
{
|
||||
if ($this->in_reply_to == null || mb_substr($this->in_reply_to, 0, 20, 'UTF-8') !== 'https://twitter.com/') {
|
||||
return;
|
||||
}
|
||||
$name = MicropubClient::where('client_url', $this->client_id)->value('client_name');
|
||||
if ($name == null) {
|
||||
$url = parse_url($this->client_id);
|
||||
if (isset($url['path'])) {
|
||||
return $url['host'] . $url['path'];
|
||||
}
|
||||
|
||||
return $url['host'];
|
||||
$arr = explode('/', $url);
|
||||
$tweetId = end($arr);
|
||||
if (Cache::has($tweetId)) {
|
||||
return Cache::get($tweetId);
|
||||
}
|
||||
try {
|
||||
$oEmbed = Twitter::getOembed([
|
||||
'id' => $tweetId,
|
||||
'align' => 'center',
|
||||
'omit_script' => true,
|
||||
'maxwidth' => 550,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
}
|
||||
Cache::put($tweetId, $oEmbed, ($oEmbed->cache_age / 60));
|
||||
|
||||
return $name;
|
||||
return $oEmbed;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -284,4 +365,61 @@ class Note extends Model
|
|||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a reverse geocode lookup of a `lat,lng` value.
|
||||
*
|
||||
* @param float The latitude
|
||||
* @param float The longitude
|
||||
* @return string The location HTML
|
||||
*/
|
||||
public function reverseGeoCode(float $latitude, float $longitude): string
|
||||
{
|
||||
$latlng = $latitude . ',' . $longitude;
|
||||
|
||||
return Cache::get($latlng, function () use ($latlng, $latitude, $longitude) {
|
||||
$guzzle = new Client();
|
||||
$response = $guzzle->request('GET', 'https://nominatim.openstreetmap.org/reverse', [
|
||||
'query' => [
|
||||
'format' => 'json',
|
||||
'lat' => $latitude,
|
||||
'lon' => $longitude,
|
||||
'zoom' => 18,
|
||||
'addressdetails' => 1,
|
||||
],
|
||||
'headers' => ['User-Agent' => 'jonnybarnes.uk via Guzzle, email jonny@jonnybarnes.uk'],
|
||||
]);
|
||||
$json = json_decode($response->getBody());
|
||||
if (isset($json->address->town)) {
|
||||
$address = '<span class="p-locality">'
|
||||
. $json->address->town
|
||||
. '</span>, <span class="p-country-name">'
|
||||
. $json->address->country
|
||||
. '</span>';
|
||||
Cache::forever($latlng, $address);
|
||||
|
||||
return $address;
|
||||
}
|
||||
if (isset($json->address->city)) {
|
||||
$address = $json->address->city . ', ' . $json->address->country;
|
||||
Cache::forever($latlng, $address);
|
||||
|
||||
return $address;
|
||||
}
|
||||
if (isset($json->address->county)) {
|
||||
$address = '<span class="p-region">'
|
||||
. $json->address->county
|
||||
. '</span>, <span class="p-country-name">'
|
||||
. $json->address->country
|
||||
. '</span>';
|
||||
Cache::forever($latlng, $address);
|
||||
|
||||
return $address;
|
||||
}
|
||||
$adress = '<span class="p-country-name">' . $json->address->country . '</span>';
|
||||
Cache::forever($latlng, $address);
|
||||
|
||||
return $address;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,19 +76,6 @@ class Place extends Model
|
|||
return $query->where($field, '<=', $distance)->orderBy($field);
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert location to text.
|
||||
*
|
||||
* @param text $value
|
||||
* @return text
|
||||
*
|
||||
public function getLocationAttribute($value)
|
||||
{
|
||||
$result = DB::select(DB::raw("SELECT ST_AsText('$value')"));
|
||||
|
||||
return $result[0]->st_astext;
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Get the latitude from the `location` property.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Jobs\AddClientToDatabase;
|
||||
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||
use App\Exceptions\InvalidTokenException;
|
||||
use Lcobucci\JWT\{Builder, Parser, Token};
|
||||
|
|
@ -26,6 +27,7 @@ class TokenService
|
|||
->set('nonce', bin2hex(random_bytes(8)))
|
||||
->sign($signer, config('app.key'))
|
||||
->getToken();
|
||||
dispatch(new AddClientToDatabase($data['client_id']));
|
||||
|
||||
return (string) $token;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,14 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use Cache;
|
||||
use Twitter;
|
||||
use HTMLPurifier;
|
||||
use Carbon\Carbon;
|
||||
use HTMLPurifier_Config;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Jonnybarnes\WebmentionsParser\Authorship;
|
||||
|
||||
class WebMention extends Model
|
||||
{
|
||||
|
|
@ -29,4 +36,112 @@ class WebMention extends Model
|
|||
* @var array
|
||||
*/
|
||||
protected $guarded = ['id'];
|
||||
|
||||
/**
|
||||
* Get the author of the webmention.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAuthorAttribute()
|
||||
{
|
||||
$authorship = new Authorship();
|
||||
$hCard = $authorship->findAuthor(json_decode($this->mf2, true));
|
||||
if (array_key_exists('properties', $hCard) &&
|
||||
array_key_exists('photo', $hCard['properties'])
|
||||
) {
|
||||
$hCard['properties']['photo'][0] = $this->createPhotoLink($hCard['properties']['photo'][0]);
|
||||
}
|
||||
|
||||
return $hCard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the published value for the webmention.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPublishedAttribute()
|
||||
{
|
||||
$microformats = json_decode($this->mf2, true);
|
||||
$carbon = new Carbon();
|
||||
if (isset($microformats['items'][0]['properties']['published'][0])) {
|
||||
try {
|
||||
$published = $carbon->parse(
|
||||
$microformats['items'][0]['properties']['published'][0]
|
||||
)->toDayDateTimeString();
|
||||
} catch (\Exception $exception) {
|
||||
$published = $webmention->updated_at->toDayDateTimeString();
|
||||
}
|
||||
} else {
|
||||
$published = $webmention->updated_at->toDayDateTimeString();
|
||||
}
|
||||
|
||||
return $published;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filteres HTML of a reply.
|
||||
*
|
||||
* @return strin|null
|
||||
*/
|
||||
public function getReplyAttribute()
|
||||
{
|
||||
$microformats = json_decode($this->mf2, true);
|
||||
if (isset($microformats['items'][0]['properties']['content'][0]['html'])) {
|
||||
return $this->filterHTML($microformats['items'][0]['properties']['content'][0]['html']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the photo link.
|
||||
*
|
||||
* @param string
|
||||
* @return string
|
||||
*/
|
||||
public function createPhotoLink(string $url): string
|
||||
{
|
||||
$url = normalize_url($url);
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
if ($host == 'pbs.twimg.com') {
|
||||
//make sure we use HTTPS, we know twitter supports it
|
||||
return str_replace('http://', 'https://', $url);
|
||||
}
|
||||
if ($host == 'twitter.com') {
|
||||
if (Cache::has($url)) {
|
||||
return Cache::get($url);
|
||||
}
|
||||
$username = parse_url($url, PHP_URL_PATH);
|
||||
try {
|
||||
$info = Twitter::getUsers(['screen_name' => $username]);
|
||||
$profile_image = $info->profile_image_url_https;
|
||||
Cache::put($url, $profile_image, 10080); //1 week
|
||||
} catch (Exception $e) {
|
||||
return $url; //not sure here
|
||||
}
|
||||
|
||||
return $profile_image;
|
||||
}
|
||||
$filesystem = new Filesystem();
|
||||
if ($filesystem->exists(public_path() . '/assets/profile-images/' . $host . '/image')) {
|
||||
return '/assets/profile-images/' . $host . '/image';
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the HTML in a reply webmention.
|
||||
*
|
||||
* @param string The reply HTML
|
||||
* @return string The filtered HTML
|
||||
*/
|
||||
private function filterHTML($html)
|
||||
{
|
||||
$config = HTMLPurifier_Config::createDefault();
|
||||
$config->set('Cache.SerializerPath', storage_path() . '/HTMLPurifier');
|
||||
$config->set('HTML.TargetBlank', true);
|
||||
$purifier = new HTMLPurifier($config);
|
||||
|
||||
return $purifier->purify($html);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
## Version 0.5.16
|
||||
## Version 0.5.17 (2017-06-22)
|
||||
- Lots of code tidying, especially in the notes controller
|
||||
- Fix issue#53 regarding uploading photos
|
||||
|
||||
## Version 0.5.16 (2017-06-17)
|
||||
- Allow place `slug`s to be re-generated
|
||||
- Add syndication links for swarm and instagram
|
||||
- Move bio to its own template, next step database?
|
||||
|
|
|
|||
42
composer.lock
generated
42
composer.lock
generated
|
|
@ -8,16 +8,16 @@
|
|||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"version": "3.29.2",
|
||||
"version": "3.29.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||
"reference": "1f19f74913a31fac8e98c24cef26040a16c88a33"
|
||||
"reference": "c60a477ad5ba1b120d4d80cbddf97fbe36573996"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/1f19f74913a31fac8e98c24cef26040a16c88a33",
|
||||
"reference": "1f19f74913a31fac8e98c24cef26040a16c88a33",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c60a477ad5ba1b120d4d80cbddf97fbe36573996",
|
||||
"reference": "c60a477ad5ba1b120d4d80cbddf97fbe36573996",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -84,7 +84,7 @@
|
|||
"s3",
|
||||
"sdk"
|
||||
],
|
||||
"time": "2017-06-09T18:57:25+00:00"
|
||||
"time": "2017-06-19T19:14:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "barnabywalters/mf-cleaner",
|
||||
|
|
@ -1580,16 +1580,16 @@
|
|||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v5.4.25",
|
||||
"version": "v5.4.27",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "6bcc9b1f542b3deed16d51f6aa1fe318ab407c2a"
|
||||
"reference": "66f5e1b37cbd66e730ea18850ded6dc0ad570404"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/6bcc9b1f542b3deed16d51f6aa1fe318ab407c2a",
|
||||
"reference": "6bcc9b1f542b3deed16d51f6aa1fe318ab407c2a",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/66f5e1b37cbd66e730ea18850ded6dc0ad570404",
|
||||
"reference": "66f5e1b37cbd66e730ea18850ded6dc0ad570404",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -1705,7 +1705,7 @@
|
|||
"framework",
|
||||
"laravel"
|
||||
],
|
||||
"time": "2017-06-07T13:35:12+00:00"
|
||||
"time": "2017-06-15T19:08:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/scout",
|
||||
|
|
@ -2144,16 +2144,16 @@
|
|||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
"version": "1.22.1",
|
||||
"version": "1.23.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Seldaek/monolog.git",
|
||||
"reference": "1e044bc4b34e91743943479f1be7a1d5eb93add0"
|
||||
"reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/1e044bc4b34e91743943479f1be7a1d5eb93add0",
|
||||
"reference": "1e044bc4b34e91743943479f1be7a1d5eb93add0",
|
||||
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4",
|
||||
"reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -2174,7 +2174,7 @@
|
|||
"phpunit/phpunit-mock-objects": "2.3.0",
|
||||
"ruflin/elastica": ">=0.90 <3.0",
|
||||
"sentry/sentry": "^0.13",
|
||||
"swiftmailer/swiftmailer": "~5.3"
|
||||
"swiftmailer/swiftmailer": "^5.3|^6.0"
|
||||
},
|
||||
"suggest": {
|
||||
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
|
||||
|
|
@ -2218,7 +2218,7 @@
|
|||
"logging",
|
||||
"psr-3"
|
||||
],
|
||||
"time": "2017-03-13T07:08:03+00:00"
|
||||
"time": "2017-06-19T01:22:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mtdowling/cron-expression",
|
||||
|
|
@ -2725,16 +2725,16 @@
|
|||
},
|
||||
{
|
||||
"name": "psy/psysh",
|
||||
"version": "v0.8.6",
|
||||
"version": "v0.8.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bobthecow/psysh.git",
|
||||
"reference": "7028d6d525fb183d50b249b7c07598e3d386b27d"
|
||||
"reference": "be969b9dc89dcaefdb9a3117fa91fa38bca19f50"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/7028d6d525fb183d50b249b7c07598e3d386b27d",
|
||||
"reference": "7028d6d525fb183d50b249b7c07598e3d386b27d",
|
||||
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/be969b9dc89dcaefdb9a3117fa91fa38bca19f50",
|
||||
"reference": "be969b9dc89dcaefdb9a3117fa91fa38bca19f50",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -2794,7 +2794,7 @@
|
|||
"interactive",
|
||||
"shell"
|
||||
],
|
||||
"time": "2017-06-04T10:34:20+00:00"
|
||||
"time": "2017-06-20T12:51:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ramsey/uuid",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class WebMentionsTableSeeder extends Seeder
|
|||
public function run()
|
||||
{
|
||||
$webmention = App\WebMention::create([
|
||||
'source' => 'https://aaornpk.local/reply/1',
|
||||
'source' => 'https://aaornpk.localhost/reply/1',
|
||||
'target' => 'https://jonnybarnes.localhost/notes/D',
|
||||
'commentable_id' => '13',
|
||||
'commentable_type' => 'App\Note',
|
||||
|
|
|
|||
4430
package-lock.json
generated
Normal file
4430
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
|
@ -6,23 +6,23 @@
|
|||
"license": "CC0-1.0",
|
||||
"dependencies": {
|
||||
"alertify.js": "^1.0.12",
|
||||
"mapbox-gl": "0.37.0",
|
||||
"mapbox-gl": "^0.38.0",
|
||||
"marked": "^0.3.6",
|
||||
"normalize.css": "7.0.0",
|
||||
"webStorage": "^1.2.2"
|
||||
"normalize.css": "^7.0.0",
|
||||
"webStorage": "^1.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-core": "^6.21.0",
|
||||
"babel-loader": "7.0.0",
|
||||
"babel-loader": "^7.1.0",
|
||||
"babel-preset-env": "^1.2.2",
|
||||
"babel-preset-es2015": "^6.18.0",
|
||||
"babel-preset-latest": "^6.16.0",
|
||||
"babel-runtime": "^6.20.0",
|
||||
"lint-staged": "^3.2.1",
|
||||
"lint-staged": "^4.0.0",
|
||||
"pre-commit": "^1.1.3",
|
||||
"stylelint-config-standard": "^16.0.0",
|
||||
"webpack": "^2.2.0"
|
||||
"webpack": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"compress": "scripts/compress",
|
||||
|
|
|
|||
12
public/assets/frontend/mapbox-gl.css
vendored
12
public/assets/frontend/mapbox-gl.css
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
2
public/assets/js/links.js
vendored
2
public/assets/js/links.js
vendored
|
|
@ -1,2 +1,2 @@
|
|||
!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={i:moduleId,l:!1,exports:{}};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.l=!0,module.exports}var installedModules={};__webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.i=function(value){return value},__webpack_require__.d=function(exports,name,getter){__webpack_require__.o(exports,name)||Object.defineProperty(exports,name,{configurable:!1,enumerable:!0,get:getter})},__webpack_require__.n=function(module){var getter=module&&module.__esModule?function(){return module.default}:function(){return module};return __webpack_require__.d(getter,"a",getter),getter},__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)},__webpack_require__.p="",__webpack_require__(__webpack_require__.s=8)}({8:function(module,exports,__webpack_require__){"use strict";var youtubeRegex=/watch\?v=([A-Za-z0-9\-_]+)\b/,spotifyRegex=/https\:\/\/play\.spotify\.com\/(.*)\b/,notes=document.querySelectorAll(".e-content"),_iteratorNormalCompletion=!0,_didIteratorError=!1,_iteratorError=void 0;try{for(var _step,_iterator=notes[Symbol.iterator]();!(_iteratorNormalCompletion=(_step=_iterator.next()).done);_iteratorNormalCompletion=!0){var note=_step.value,ytid=note.textContent.match(youtubeRegex);if(ytid){var ytcontainer=document.createElement("div");ytcontainer.classList.add("container");var ytiframe=document.createElement("iframe");ytiframe.classList.add("youtube"),ytiframe.setAttribute("src","https://www.youtube.com/embed/"+ytid[1]),ytiframe.setAttribute("frameborder",0),ytiframe.setAttribute("allowfullscreen","true"),ytcontainer.appendChild(ytiframe),note.appendChild(ytcontainer)}var spotifyid=note.textContent.match(spotifyRegex);if(spotifyid){var sid=spotifyid[1].replace("/",":"),siframe=document.createElement("iframe");siframe.classList.add("spotify"),siframe.setAttribute("src","https://embed.spotify.com/?uri=spotify:"+sid),siframe.setAttribute("frameborder",0),siframe.setAttribute("allowtransparency","true"),note.appendChild(siframe)}}}catch(err){_didIteratorError=!0,_iteratorError=err}finally{try{!_iteratorNormalCompletion&&_iterator.return&&_iterator.return()}finally{if(_didIteratorError)throw _iteratorError}}}});
|
||||
!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={i:moduleId,l:!1,exports:{}};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.l=!0,module.exports}var installedModules={};__webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.d=function(exports,name,getter){__webpack_require__.o(exports,name)||Object.defineProperty(exports,name,{configurable:!1,enumerable:!0,get:getter})},__webpack_require__.n=function(module){var getter=module&&module.__esModule?function(){return module.default}:function(){return module};return __webpack_require__.d(getter,"a",getter),getter},__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)},__webpack_require__.p="",__webpack_require__(__webpack_require__.s=5)}({5:function(module,exports,__webpack_require__){"use strict";var youtubeRegex=/watch\?v=([A-Za-z0-9\-_]+)\b/,spotifyRegex=/https\:\/\/play\.spotify\.com\/(.*)\b/,notes=document.querySelectorAll(".e-content"),_iteratorNormalCompletion=!0,_didIteratorError=!1,_iteratorError=void 0;try{for(var _step,_iterator=notes[Symbol.iterator]();!(_iteratorNormalCompletion=(_step=_iterator.next()).done);_iteratorNormalCompletion=!0){var note=_step.value,ytid=note.textContent.match(youtubeRegex);if(ytid){var ytcontainer=document.createElement("div");ytcontainer.classList.add("container");var ytiframe=document.createElement("iframe");ytiframe.classList.add("youtube"),ytiframe.setAttribute("src","https://www.youtube.com/embed/"+ytid[1]),ytiframe.setAttribute("frameborder",0),ytiframe.setAttribute("allowfullscreen","true"),ytcontainer.appendChild(ytiframe),note.appendChild(ytcontainer)}var spotifyid=note.textContent.match(spotifyRegex);if(spotifyid){var sid=spotifyid[1].replace("/",":"),siframe=document.createElement("iframe");siframe.classList.add("spotify"),siframe.setAttribute("src","https://embed.spotify.com/?uri=spotify:"+sid),siframe.setAttribute("frameborder",0),siframe.setAttribute("allowtransparency","true"),note.appendChild(siframe)}}}catch(err){_didIteratorError=!0,_iteratorError=err}finally{try{!_iteratorNormalCompletion&&_iterator.return&&_iterator.return()}finally{if(_didIteratorError)throw _iteratorError}}}});
|
||||
//# sourceMappingURL=links.js.map
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue