contacts = null; } /** * The database table used by the model. * * @var string */ protected $table = 'notes'; /* * Mass-assignment * * @var array */ protected $fillable = [ 'note', 'in_reply_to', 'client_id', ]; /** * Hide the column used with Laravel Scout. * * @var array */ protected $hidden = ['searchable']; /** * Define the relationship with tags. * * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function tags() { return $this->belongsToMany('App\Models\Tag'); } /** * Define the relationship with clients. * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function client() { return $this->belongsTo('App\Models\MicropubClient', 'client_id', 'client_url'); } /** * Define the relationship with webmentions. * * @return \Illuminate\Database\Eloquent\Relations\MorphMany */ public function webmentions() { return $this->morphMany('App\Models\WebMention', 'commentable'); } /** * Define the relationship with places. * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function place() { return $this->belongsTo('App\Models\Place'); } /** * Define the relationship with media. * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function media() { return $this->hasMany('App\Models\Media'); } /** * Set the attributes to be indexed for searching with Scout. * * @return array */ public function toSearchableArray(): array { return [ 'note' => $this->note, ]; } /** * Normalize the note to Unicode FORM C. * * @param string|null $value */ public function setNoteAttribute(?string $value) { if ($value !== null) { $normalized = normalizer_normalize($value, Normalizer::FORM_C); if ($normalized === '') { //we don’t want to save empty strings to the db $normalized = null; } $this->attributes['note'] = $normalized; } } /** * Pre-process notes for web-view. * * @param string|null $value * @return string|null */ public function getNoteAttribute(?string $value): ?string { if ($value === null && $this->place !== null) { $value = '📍: ' . $this->place->name . ''; } // if $value is still null, just return null if ($value === null) { return null; } $hcards = $this->makeHCards($value); $hashtags = $this->autoLinkHashtag($hcards); $html = $this->convertMarkdown($hashtags); $modified = resolve(EmojiModifier::class)->makeEmojiAccessible($html); return $modified; } /** * Generate the NewBase60 ID from primary ID. * * @return string */ public function getNb60idAttribute(): string { // we cast to string because sometimes the nb60id is an “int” return (string) resolve(Numbers::class)->numto60($this->id); } /** * The Long URL for a note. * * @return string */ public function getLongurlAttribute(): string { return config('app.url') . '/notes/' . $this->nb60id; } /** * The Short URL for a note. * * @return string */ public function getShorturlAttribute(): string { return config('app.shorturl') . '/notes/' . $this->nb60id; } /** * Get the ISO8601 value for mf2. * * @return string */ public function getIso8601Attribute(): string { return $this->updated_at->toISO8601String(); } /** * Get the ISO8601 value for mf2. * * @return string */ public function getHumandiffAttribute(): string { return $this->updated_at->diffForHumans(); } /** * Get the pubdate value for RSS feeds. * * @return string */ public function getPubdateAttribute(): string { return $this->updated_at->toRSSString(); } /** * Get the latitude value. * * @return float|null */ public function getLatitudeAttribute(): ?float { if ($this->place !== null) { return $this->place->location->getLat(); } if ($this->location !== null) { $pieces = explode(':', $this->location); $latlng = explode(',', $pieces[0]); return (float) trim($latlng[0]); } return null; } /** * Get the longitude value. * * @return float|null */ public function getLongitudeAttribute(): ?float { if ($this->place !== null) { return $this->place->location->getLng(); } if ($this->location !== null) { $pieces = explode(':', $this->location); $latlng = explode(',', $pieces[0]); return (float) trim($latlng[1]); } return null; } /** * 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(): ?string { if ($this->place !== null) { return $this->place->name; } if ($this->location !== null) { return $this->reverseGeoCode((float) $this->latitude, (float) $this->longitude); } return null; } /** * Get the OEmbed html for a tweet the note is a reply to. * * @return object|null */ public function getTwitterAttribute(): ?object { if ($this->in_reply_to == null || mb_substr($this->in_reply_to, 0, 20, 'UTF-8') !== 'https://twitter.com/') { return null; } $tweetId = basename($this->in_reply_to); if (Cache::has($tweetId)) { return Cache::get($tweetId); } try { $oEmbed = Twitter::getOembed([ 'url' => $this->in_reply_to, 'dnt' => true, 'align' => 'center', 'maxwidth' => 512, ]); } catch (\Exception $e) { return null; } Cache::put($tweetId, $oEmbed, ($oEmbed->cache_age / 60)); return $oEmbed; } /** * Show a specific form of the note for twitter. * * That is we swap the contacts names for their known Twitter handles. * * @return string|null */ public function getTwitterContentAttribute(): ?string { if ($this->contacts === null) { return null; } if (count($this->contacts) === 0) { return null; } if (count(array_unique(array_values($this->contacts))) === 1 && array_unique(array_values($this->contacts))[0] === null) { return null; } // swap in twitter usernames $swapped = preg_replace_callback( self::USERNAMES_REGEX, function ($matches) { if (is_null($this->contacts[$matches[1]])) { return $matches[0]; } $contact = $this->contacts[$matches[1]]; if ($contact->twitter) { return '@' . $contact->twitter; } return $contact->name; }, $this->getOriginal('note') ); return $this->convertMarkdown($swapped); } /** * Show a specific form of the note for facebook. * * That is we swap the contacts names for their known Facebook usernames. * * @return string|null */ public function getFacebookContentAttribute(): ?string { if (count($this->contacts) === 0) { return null; } if (count(array_unique(array_values($this->contacts))) === 1 && array_unique(array_values($this->contacts))[0] === null) { return null; } // swap in facebook usernames $swapped = preg_replace_callback( self::USERNAMES_REGEX, function ($matches) { if (is_null($this->contacts[$matches[1]])) { return $matches[0]; } $contact = $this->contacts[$matches[1]]; if ($contact->facebook) { return '' . $contact->name . ''; } return $contact->name; }, $this->getOriginal('note') ); return $this->convertMarkdown($swapped); } /** * Scope a query to select a note via a NewBase60 id. * * @param \Illuminate\Database\Eloquent\Builder $query * @param string $nb60id * @return \Illuminate\Database\Eloquent\Builder */ public function scopeNb60(Builder $query, string $nb60id): Builder { return $query->where('id', resolve(Numbers::class)->b60tonum($nb60id)); } /** * Swap contact’s nicks for a full mf2 h-card. * * Take note that this method does two things, given @username (NOT [@username](URL)!) * we try to create a fancy hcard from our contact info. If this is not possible * due to lack of contact info, we assume @username is a twitter handle and link it * as such. * * @param string $text * @return string */ private function makeHCards(string $text): string { $this->getContacts(); if (count($this->contacts) === 0) { return $text; } $hcards = preg_replace_callback( self::USERNAMES_REGEX, function ($matches) { if (is_null($this->contacts[$matches[1]])) { return '' . $matches[0] . ''; } $contact = $this->contacts[$matches[1]]; // easier to read the following code $host = parse_url($contact->homepage, PHP_URL_HOST); $contact->photo = '/assets/profile-images/default-image'; if (file_exists(public_path() . '/assets/profile-images/' . $host . '/image')) { $contact->photo = '/assets/profile-images/' . $host . '/image'; } return trim(view('templates.mini-hcard', ['contact' => $contact])->render()); }, $text ); return $hcards; } /** * Get the value of the `contacts` property. */ public function getContacts() { if ($this->contacts === null) { $this->setContacts(); } } /** * Process the note and save the contacts to the `contacts` property. */ public function setContacts() { $contacts = []; if ($this->getOriginal('note')) { preg_match_all(self::USERNAMES_REGEX, $this->getoriginal('note'), $matches); foreach ($matches[1] as $match) { $contacts[$match] = Contact::where('nick', mb_strtolower($match))->first(); } } $this->contacts = $contacts; } /** * Turn text hashtags to full HTML links. * * Given a string and section, finds all hashtags matching * `#[\-_a-zA-Z0-9]+` and wraps them in an `a` element with * `rel=tag` set and a `href` of 'section/tagged/' + tagname without the #. * * @param string $note * @return string */ public function autoLinkHashtag(string $note): string { return preg_replace_callback( '/#([^\s]*)\b/', function ($matches) { return ''; }, $note ); } /** * Pass a note through the commonmark library. * * @param string $note * @return string */ private function convertMarkdown(string $note): string { $environment = Environment::createCommonMarkEnvironment(); $environment->addExtension(new LinkifyExtension()); $converter = new Converter(new DocParser($environment), new HtmlRenderer($environment)); return $converter->convertToHtml($note); } /** * Do a reverse geocode lookup of a `lat,lng` value. * * @param float $latitude * @param float $longitude * @return string */ 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((string) $response->getBody()); if (isset($json->address->town)) { $address = '' . $json->address->town . ', ' . $json->address->country . ''; 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 = '' . $json->address->county . ', ' . $json->address->country . ''; Cache::forever($latlng, $address); return $address; } $address = '' . $json->address->country . ''; Cache::forever($latlng, $address); return $address; }); } }