From 21979bb7d75ddbc0edfd529779a6f368bd0df7d5 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Fri, 23 Oct 2015 21:15:40 +0000 Subject: [PATCH] Allow users to @mention URLs Because inferring who you mean (especially in the presence of remotes) can suck --- classes/Profile.php | 42 ++++++++++++++++++ lib/util.php | 106 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 128 insertions(+), 20 deletions(-) diff --git a/classes/Profile.php b/classes/Profile.php index 5ef77a9506..f3252f2f60 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -1520,6 +1520,48 @@ class Profile extends Managed_DataObject return $profile; } + /* + * Get or create a profile given a possible URL + */ + static function ensureFromUrl($url, $mf2=null) { + common_debug('Trying to find a profile for ' . $url); + + $url = preg_replace('#https?://#', 'https://', $url); + try { + $profile = Profile::fromUri($url); + } catch(UnknownUriException $ex) {} + + if(!($profile instanceof Profile)) { + $profile = Profile::getKV('profileurl', $url); + } + + $url = str_replace('https://', 'http://', $url); + if(!($profile instanceof Profile)) { + try { + $profile = Profile::fromUri($url); + } catch(UnknownUriException $ex) {} + } + + if(!($profile instanceof Profile)) { + $profile = Profile::getKV('profileurl', $url); + } + + if(!($profile instanceof Profile)) { + $hcard = common_representative_hcard($url, null, $mf2); + if(!$hcard) return null; + + $profile = new Profile(); + $profile->profileurl = $hcard['url'][0]; + $profile->fullname = $hcard['name'][0]; + preg_match_all('/'.Nickname::DISPLAY_FMT.'/', $profile->fullname, $altnick); + $profile->nickname = $hcard['nickname'] ? $hcard['nickname'][0] : implode($altnick[0]); + $profile->created = common_sql_now(); + $profile->insert(); + } + + return $profile; + } + function canRead(Notice $notice) { if ($notice->scope & Notice::SITE_SCOPE) { diff --git a/lib/util.php b/lib/util.php index 66847a4350..b842bbaa93 100644 --- a/lib/util.php +++ b/lib/util.php @@ -648,7 +648,7 @@ function common_linkify_mentions($text, Notice $notice) $linkText = common_linkify_mention($mention); - $text = substr_replace($text, $linkText, $position, mb_strlen($mention['text'])); + $text = substr_replace($text, $linkText, $position, $mention['length']); } return $text; @@ -730,24 +730,33 @@ function common_find_mentions($text, Notice $notice) $matches = common_find_mentions_raw($text); foreach ($matches as $match) { - try { - $nickname = Nickname::normalize($match[0]); - } catch (NicknameException $e) { - // Bogus match? Drop it. - continue; - } - - // Try to get a profile for this nickname. - // Start with conversation context, then go to - // sender context. - - if ($origAuthor instanceof Profile && $origAuthor->nickname == $nickname) { - $mentioned = $origAuthor; - } else if (!empty($origMentions) && - array_key_exists($nickname, $origMentions)) { - $mentioned = $origMentions[$nickname]; + // Try to process it as @URL + $url = $match[0]; + if(!common_valid_http_url($url)) { $url = 'http://' . $url; } + if(common_valid_http_url($url)) { + $mentioned = Profile::ensureFromUrl($url); + $text = mb_strlen($mentioned->nickname) <= mb_strlen($match[0]) ? $mentioned->nickname : $match[0]; } else { - $mentioned = common_relative_profile($sender, $nickname); + try { + $nickname = Nickname::normalize($match[0]); + } catch (NicknameException $e) { + // Bogus match? Drop it. + continue; + } + + // Try to get a profile for this nickname. + // Start with conversation context, then go to + // sender context. + + if ($origAuthor instanceof Profile && $origAuthor->nickname == $nickname) { + $mentioned = $origAuthor; + } else if (!empty($origMentions) && + array_key_exists($nickname, $origMentions)) { + $mentioned = $origMentions[$nickname]; + } else { + $mentioned = common_relative_profile($sender, $nickname); + } + $text = $match[0]; } if ($mentioned instanceof Profile) { @@ -761,8 +770,9 @@ function common_find_mentions($text, Notice $notice) $mention = array('mentioned' => array($mentioned), 'type' => 'mention', - 'text' => $match[0], + 'text' => $text, 'position' => $match[1], + 'length' => mb_strlen($match[0]), 'url' => $url); if (!empty($mentioned->fullname)) { @@ -838,7 +848,7 @@ function common_find_mentions_raw($text) PREG_OFFSET_CAPTURE); $atmatches = array(); - preg_match_all('/(?:^|\s+)@(' . Nickname::DISPLAY_FMT . ')\b/', + preg_match_all('/(?:^|\s+)@((?:[A-Za-z0-9_:\-\.\/%]+)|(?:' . Nickname::DISPLAY_FMT . '))\b/', $text, $atmatches, PREG_OFFSET_CAPTURE); @@ -2430,6 +2440,62 @@ function common_strip_html($html, $trim=true, $save_whitespace=false) return $trim ? trim($text) : $text; } +function common_representative_hcard($url, $fn=null, $mf2=null) { + if(!$mf2) { + $request = HTTPClient::start(); + + try { + $response = $request->get($url); + } catch(Exception $ex) { + return null; + } + + $url = $response->getEffectiveUrl(); + $mf2 = new Mf2\Parser($response->getBody(), $url); + $mf2 = $mf2->parse(); + } + + $hcard = null; + + if(!empty($mf2['items'])) { + $hcards = array(); + foreach($mf2['items'] as $item) { + if(!in_array('h-card', $item['type'])) { + continue; + } + + // We found a match, return it immediately + if(isset($item['properties']['url']) && in_array($url, $item['properties']['url'])) { + $hcard = $item['properties']; + break; + } + + // Let's keep all the hcards for later, to return one of them at least + $hcards[] = $item['properties']; + } + + // No match immediately for the url we expected, but there were h-cards found + if (count($hcards) > 0) { + $hcard = $hcards[0]; + } + } + + if(!$hcard && $fn) { + $hcard = array('name' => array($fn)); + } + + if(!$hcard && $response) { + preg_match('/([^<]+)/', $response->getBody(), $match); + $hcard = array('name' => array($match[1])); + } + + if($hcard && !$hcard['url']) { + $hcard['url'] = array($url); + } + + return $hcard; +} + function html_sprintf() { $args = func_get_args();