Merge pull request #1211 from MrPetovan/task/11022-twitter-support-delete

[twitter] Add support for unretweet and post/comment deletion
This commit is contained in:
Philipp 2021-11-27 13:29:19 +01:00 committed by GitHub
commit 13ce3aa0a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 178 additions and 49 deletions

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-08 22:25-0400\n" "POT-Creation-Date: 2021-11-23 18:33-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,27 +17,27 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: twitter.php:224 #: twitter.php:213
msgid "Post to Twitter" msgid "Post to Twitter"
msgstr "" msgstr ""
#: twitter.php:269 #: twitter.php:258
msgid "" msgid ""
"You submitted an empty PIN, please Sign In with Twitter again to get a new " "You submitted an empty PIN, please Sign In with Twitter again to get a new "
"one." "one."
msgstr "" msgstr ""
#: twitter.php:329 twitter.php:333 #: twitter.php:318 twitter.php:322
msgid "Twitter Import/Export/Mirror" msgid "Twitter Import/Export/Mirror"
msgstr "" msgstr ""
#: twitter.php:340 #: twitter.php:329
msgid "" msgid ""
"No consumer key pair for Twitter found. Please contact your site " "No consumer key pair for Twitter found. Please contact your site "
"administrator." "administrator."
msgstr "" msgstr ""
#: twitter.php:352 #: twitter.php:341
msgid "" msgid ""
"At this Friendica instance the Twitter addon was enabled but you have not " "At this Friendica instance the Twitter addon was enabled but you have not "
"yet connected your account to your Twitter account. To do so click the " "yet connected your account to your Twitter account. To do so click the "
@ -46,42 +46,42 @@ msgid ""
"be posted to Twitter." "be posted to Twitter."
msgstr "" msgstr ""
#: twitter.php:353 #: twitter.php:342
msgid "Log in with Twitter" msgid "Log in with Twitter"
msgstr "" msgstr ""
#: twitter.php:355 #: twitter.php:344
msgid "Copy the PIN from Twitter here" msgid "Copy the PIN from Twitter here"
msgstr "" msgstr ""
#: twitter.php:360 twitter.php:415 twitter.php:803 #: twitter.php:349 twitter.php:404 twitter.php:924
msgid "Save Settings" msgid "Save Settings"
msgstr "" msgstr ""
#: twitter.php:362 twitter.php:417 #: twitter.php:351 twitter.php:406
msgid "An error occured: " msgid "An error occured: "
msgstr "" msgstr ""
#: twitter.php:379 #: twitter.php:368
msgid "Currently connected to: " msgid "Currently connected to: "
msgstr "" msgstr ""
#: twitter.php:380 twitter.php:390 #: twitter.php:369 twitter.php:379
msgid "Disconnect" msgid "Disconnect"
msgstr "" msgstr ""
#: twitter.php:397 #: twitter.php:386
msgid "Allow posting to Twitter" msgid "Allow posting to Twitter"
msgstr "" msgstr ""
#: twitter.php:397 #: twitter.php:386
msgid "" msgid ""
"If enabled all your <strong>public</strong> postings can be posted to the " "If enabled all your <strong>public</strong> postings can be posted to the "
"associated Twitter account. You can choose to do so by default (here) or for " "associated Twitter account. You can choose to do so by default (here) or for "
"every posting separately in the posting options when writing the entry." "every posting separately in the posting options when writing the entry."
msgstr "" msgstr ""
#: twitter.php:400 #: twitter.php:389
msgid "" msgid ""
"<strong>Note</strong>: Due to your privacy settings (<em>Hide your profile " "<strong>Note</strong>: Due to your privacy settings (<em>Hide your profile "
"details from unknown viewers?</em>) the link potentially included in public " "details from unknown viewers?</em>) the link potentially included in public "
@ -89,23 +89,23 @@ msgid ""
"the visitor that the access to your profile has been restricted." "the visitor that the access to your profile has been restricted."
msgstr "" msgstr ""
#: twitter.php:403 #: twitter.php:392
msgid "Send public postings to Twitter by default" msgid "Send public postings to Twitter by default"
msgstr "" msgstr ""
#: twitter.php:406 #: twitter.php:395
msgid "Mirror all posts from twitter that are no replies" msgid "Mirror all posts from twitter that are no replies"
msgstr "" msgstr ""
#: twitter.php:409 #: twitter.php:398
msgid "Import the remote timeline" msgid "Import the remote timeline"
msgstr "" msgstr ""
#: twitter.php:412 #: twitter.php:401
msgid "Automatically create contacts" msgid "Automatically create contacts"
msgstr "" msgstr ""
#: twitter.php:412 #: twitter.php:401
msgid "" msgid ""
"This will automatically create a contact in Friendica as soon as you receive " "This will automatically create a contact in Friendica as soon as you receive "
"a message from an existing contact via the Twitter network. If you do not " "a message from an existing contact via the Twitter network. If you do not "
@ -113,15 +113,25 @@ msgid ""
"from whom you would like to see posts here." "from whom you would like to see posts here."
msgstr "" msgstr ""
#: twitter.php:805 #: twitter.php:557
msgid ""
"Please connect a Twitter account in your Social Network settings to import "
"Twitter posts."
msgstr ""
#: twitter.php:564
msgid "Twitter post not found."
msgstr ""
#: twitter.php:926
msgid "Consumer key" msgid "Consumer key"
msgstr "" msgstr ""
#: twitter.php:806 #: twitter.php:927
msgid "Consumer secret" msgid "Consumer secret"
msgstr "" msgstr ""
#: twitter.php:1002 #: twitter.php:1123
#, php-format #, php-format
msgid "%s on Twitter" msgid "%s on Twitter"
msgstr "" msgstr ""

View file

@ -114,6 +114,7 @@ function twitter_install()
Hook::register('prepare_body' , __FILE__, 'twitter_prepare_body'); Hook::register('prepare_body' , __FILE__, 'twitter_prepare_body');
Hook::register('check_item_notification', __FILE__, 'twitter_check_item_notification'); Hook::register('check_item_notification', __FILE__, 'twitter_check_item_notification');
Hook::register('probe_detect' , __FILE__, 'twitter_probe_detect'); Hook::register('probe_detect' , __FILE__, 'twitter_probe_detect');
Hook::register('item_by_link' , __FILE__, 'twitter_item_by_link');
Hook::register('parse_link' , __FILE__, 'twitter_parse_link'); Hook::register('parse_link' , __FILE__, 'twitter_parse_link');
Logger::info("installed twitter"); Logger::info("installed twitter");
} }
@ -411,25 +412,35 @@ function twitter_settings(App $a, &$s)
function twitter_hook_fork(App $a, array &$b) function twitter_hook_fork(App $a, array &$b)
{ {
DI::logger()->debug('twitter_hook_fork', $b);
if ($b['name'] != 'notifier_normal') { if ($b['name'] != 'notifier_normal') {
return; return;
} }
$post = $b['data']; $post = $b['data'];
// Deleting and editing is not supported by the addon (deleting could, but isn't by now) // Deletion checks are done in twitter_delete_item()
if ($post['deleted'] || ($post['created'] !== $post['edited'])) { if ($post['deleted']) {
return;
}
// Editing is not supported by the addon
if ($post['created'] !== $post['edited']) {
DI::logger()->info('Editing is not supported by the addon');
$b['execute'] = false; $b['execute'] = false;
return; return;
} }
// if post comes from twitter don't send it back // if post comes from twitter don't send it back
if (($post['extid'] == Protocol::TWITTER) || twitter_get_id($post['extid'])) { if (($post['extid'] == Protocol::TWITTER) || twitter_get_id($post['extid'])) {
DI::logger()->info('If post comes from twitter don\'t send it back');
$b['execute'] = false; $b['execute'] = false;
return; return;
} }
if (substr($post['app'], 0, 7) == 'Twitter') { if (substr($post['app'], 0, 7) == 'Twitter') {
DI::logger()->info('No Twitter app');
$b['execute'] = false; $b['execute'] = false;
return; return;
} }
@ -444,6 +455,7 @@ function twitter_hook_fork(App $a, array &$b)
} else { } else {
// Comments are never exported when we don't import the twitter timeline // Comments are never exported when we don't import the twitter timeline
if (!strstr($post['postopts'], 'twitter') || ($post['parent'] != $post['id']) || $post['private']) { if (!strstr($post['postopts'], 'twitter') || ($post['parent'] != $post['id']) || $post['private']) {
DI::logger()->info('Comments are never exported when we don\'t import the twitter timeline');
$b['execute'] = false; $b['execute'] = false;
return; return;
} }
@ -482,7 +494,7 @@ function twitter_post_local(App $a, array &$b)
function twitter_probe_detect(App $a, array &$hookData) function twitter_probe_detect(App $a, array &$hookData)
{ {
// Don't overwrite an existing result // Don't overwrite an existing result
if ($hookData['result']) { if (isset($hookData['result'])) {
return; return;
} }
@ -494,6 +506,13 @@ function twitter_probe_detect(App $a, array &$hookData)
if (preg_match('=([^@]+)@(?:mobile\.)?twitter\.com$=i', $hookData['uri'], $matches)) { if (preg_match('=([^@]+)@(?:mobile\.)?twitter\.com$=i', $hookData['uri'], $matches)) {
$nick = $matches[1]; $nick = $matches[1];
} elseif (preg_match('=^https?://(?:mobile\.)?twitter\.com/(.+)=i', $hookData['uri'], $matches)) { } elseif (preg_match('=^https?://(?:mobile\.)?twitter\.com/(.+)=i', $hookData['uri'], $matches)) {
if (strpos($matches[1], '/') !== false) {
// Status case: https://twitter.com/<nick>/status/<status id>
// Not a contact
$hookData['result'] = false;
return;
}
$nick = $matches[1]; $nick = $matches[1];
} else { } else {
return; return;
@ -506,6 +525,52 @@ function twitter_probe_detect(App $a, array &$hookData)
} }
} }
function twitter_item_by_link(App $a, array &$hookData)
{
// Don't overwrite an existing result
if (isset($hookData['item_id'])) {
return;
}
// Relevancy check
if (!preg_match('#^https?://(?:mobile\.|www\.)?twitter.com/[^/]+/status/(\d+).*#', $hookData['uri'], $matches)) {
return;
}
// From now on, any early return should abort the whole chain since we've established it was a Twitter URL
$hookData['item_id'] = false;
// Node-level configuration check
if (empty(DI::config()->get('twitter', 'consumerkey')) || empty(DI::config()->get('twitter', 'consumersecret'))) {
return;
}
// No anonymous import
if (!$hookData['uid']) {
return;
}
if (
empty(DI::pConfig()->get($hookData['uid'], 'twitter', 'oauthtoken'))
|| empty(DI::pConfig()->get($hookData['uid'], 'twitter', 'oauthsecret'))
) {
notice(DI::l10n()->t('Please connect a Twitter account in your Social Network settings to import Twitter posts.'));
return;
}
$status = twitter_statuses_show($matches[1]);
if (empty($status->id_str)) {
notice(DI::l10n()->t('Twitter post not found.'));
return;
}
$item = twitter_createpost($a, $hookData['uid'], $status, [], true, false, false);
if (!empty($item)) {
$hookData['item_id'] = Item::insert($item);
}
}
function twitter_api_post(string $apiPath, string $pid, int $uid): ?bool function twitter_api_post(string $apiPath, string $pid, int $uid): ?bool
{ {
if (empty($pid)) { if (empty($pid)) {
@ -568,9 +633,16 @@ function twitter_get_id(string $uri)
function twitter_post_hook(App $a, array &$b) function twitter_post_hook(App $a, array &$b)
{ {
DI::logger()->info('twitter_post_hook', $b);
if ($b['deleted']) {
twitter_delete_item($b);
return;
}
// Post to Twitter // Post to Twitter
if (!DI::pConfig()->get($b["uid"], 'twitter', 'import') if (!DI::pConfig()->get($b["uid"], 'twitter', 'import')
&& ($b['deleted'] || $b['private'] || ($b['created'] !== $b['edited']))) { && ($b['private'] || ($b['created'] !== $b['edited']))) {
return; return;
} }
@ -620,39 +692,21 @@ function twitter_post_hook(App $a, array &$b)
} }
} }
/**
* @TODO This can't work at the moment:
* - Posts created on Friendica and mirrored to Twitter don't have a Twitter ID
* - Posts created on Twitter and mirrored on Friendica do not trigger the notifier hook this is part of.
*/
//if (($b['verb'] == Activity::POST) && $b['deleted']) {
// twitter_api_post('statuses/destroy', twitter_get_id($thr_parent['uri']), $b['uid']);
//}
if ($b['verb'] == Activity::LIKE) { if ($b['verb'] == Activity::LIKE) {
Logger::info('Like', ['uid' => $b['uid'], 'id' => twitter_get_id($b["thr-parent"])]); Logger::info('Like', ['uid' => $b['uid'], 'id' => twitter_get_id($b["thr-parent"])]);
twitter_api_post($b['deleted'] ? 'favorites/destroy' : 'favorites/create', twitter_get_id($b["thr-parent"]), $b["uid"]); twitter_api_post('favorites/create', twitter_get_id($b['thr-parent']), $b['uid']);
return; return;
} }
if ($b['verb'] == Activity::ANNOUNCE) { if ($b['verb'] == Activity::ANNOUNCE) {
Logger::info('Retweet', ['uid' => $b['uid'], 'id' => twitter_get_id($b["thr-parent"])]); Logger::info('Retweet', ['uid' => $b['uid'], 'id' => twitter_get_id($b["thr-parent"])]);
if ($b['deleted']) { twitter_retweet($b['uid'], twitter_get_id($b['thr-parent']));
/**
* @TODO This can't work at the moment:
* - Twitter post reshare removal doesn't seem to trigger the notifier hook this is part of
*/
//twitter_api_post('statuses/destroy', twitter_get_id($thr_parent['extid']), $b['uid']);
} else {
twitter_retweet($b["uid"], twitter_get_id($b["thr-parent"]));
}
return; return;
} }
if ($b['deleted'] || ($b['created'] !== $b['edited'])) { if ($b['created'] !== $b['edited']) {
return; return;
} }
@ -789,6 +843,71 @@ function twitter_post_hook(App $a, array &$b)
} }
} }
function twitter_delete_item(array $item)
{
if (!$item['deleted']) {
return;
}
if ($item['parent'] != $item['id']) {
Logger::debug('Deleting comment/announce', ['item' => $item]);
// Looking if it's a reply to a twitter post
if (!twitter_get_id($item['parent-uri']) &&
!twitter_get_id($item['extid']) &&
!twitter_get_id($item['thr-parent'])) {
Logger::info('No twitter post', ['parent' => $item['parent']]);
return;
}
$condition = ['uri' => $item['thr-parent'], 'uid' => $item['uid']];
$thr_parent = Post::selectFirst(['uri', 'extid', 'author-link', 'author-nick', 'author-network'], $condition);
if (!DBA::isResult($thr_parent)) {
Logger::warning('No parent found', ['thr-parent' => $item['thr-parent']]);
return;
}
Logger::debug('Parent found', ['parent' => $thr_parent]);
} else {
if (!strstr($item['extid'], 'twitter')) {
DI::logger()->info('Not a Twitter post', ['extid' => $item['extid']]);
return;
}
// Don't delete if the post doesn't belong to us.
// This is a check for forum postings
$self = DBA::selectFirst('contact', ['id'], ['uid' => $item['uid'], 'self' => true]);
if ($item['contact-id'] != $self['id']) {
DI::logger()->info('Don\'t delete if the post doesn\'t belong to the user', ['contact-id' => $item['contact-id'], 'self' => $self['id']]);
return;
}
}
/**
* @TODO Remaining caveat: Comments posted on Twitter and imported in Friendica do not trigger any Notifier task,
* possibly because they are private to the user and don't require any remote deletion notifications sent.
* Comments posted on Friendica and mirrored on Twitter trigger the Notifier task and the Twitter counter-part
* will be deleted accordingly.
*/
if ($item['verb'] == Activity::POST) {
Logger::info('Delete post/comment', ['uid' => $item['uid'], 'id' => twitter_get_id($item['extid'])]);
twitter_api_post('statuses/destroy', twitter_get_id($item['extid']), $item['uid']);
return;
}
if ($item['verb'] == Activity::LIKE) {
Logger::info('Unlike', ['uid' => $item['uid'], 'id' => twitter_get_id($item['thr-parent'])]);
twitter_api_post('favorites/destroy', twitter_get_id($item['thr-parent']), $item['uid']);
return;
}
if ($item['verb'] == Activity::ANNOUNCE && !empty($thr_parent['uri'])) {
Logger::info('Unretweet', ['uid' => $item['uid'], 'extid' => $thr_parent['uri'], 'id' => twitter_get_id($thr_parent['uri'])]);
twitter_api_post('statuses/unretweet', twitter_get_id($thr_parent['uri']), $item['uid']);
return;
}
}
function twitter_addon_admin_post(App $a) function twitter_addon_admin_post(App $a)
{ {
$consumerkey = trim($_POST['consumerkey'] ?? ''); $consumerkey = trim($_POST['consumerkey'] ?? '');