diff --git a/ratioed/RatioedPanel.php b/ratioed/RatioedPanel.php index 19f6dd44..33a009cd 100644 --- a/ratioed/RatioedPanel.php +++ b/ratioed/RatioedPanel.php @@ -8,7 +8,9 @@ use Friendica\Core\Renderer; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\User; +use Friendica\Model\Verb; use Friendica\Module\Moderation\Users\Active; +use Friendica\Protocol\Activity; /** * This class implements the "Behaviour" panel in Moderation/Users @@ -75,6 +77,11 @@ class RatioedPanel extends Active $this->t('Comments last 24h'), $this->t('Reactions last 24h'), $this->t('Ratio last 24h'), + $this->t('Replies last month'), + $this->t('Reply likes'), + $this->t('Respondee likes'), + $this->t('OP likes'), + $this->t('Reply guy score'), ]; $field_names = [ 'name', @@ -87,6 +94,11 @@ class RatioedPanel extends Active 'comments', 'reactions', 'ratio', + 'reply_count', + 'reply_likes', + 'reply_respondee_likes', + 'reply_op_likes', + 'reply_guy_score', ]; $th_users = array_map(null, $header_titles, $valid_orders, $field_names); @@ -125,6 +137,129 @@ class RatioedPanel extends Active ]); } + protected function getReplyGuyRow($contact_uid) + { + $like_vid = Verb::getID(Activity::LIKE); + $post_vid = Verb::getID(Activity::POST); + + /* + * This is a complicated query. + * + * The innermost select retrieves a chain of four posts: an + * original post, a target comment (possibly deep down in the + * thread), a reply from our user, and a like for that reply. + * If there's no like, we still want to count the reply, so we + * use an outer join. + * + * The second select adds "points" for different kinds of + * likes. The outermost select then counts up these points, + * and the number of distinct replies. + */ + $reply_guy_result = DBA::p(' +SELECT + COUNT(distinct reply_id) AS replies_total, + SUM(like_point) AS like_total, + SUM(target_like_point) AS target_like_total, + SUM(original_like_point) AS original_like_total +FROM ( + SELECT + reply_id, + like_date, + like_date IS NOT NULL AS like_point, + like_author = target_author AS target_like_point, + like_author = original_author AS original_like_point + FROM ( + SELECT + original_post.`uri-id` AS original_id, + original_post.`author-id` AS original_author, + original_post.created AS original_date, + target_post.`uri-id` AS target_id, + target_post.`author-id` AS target_author, + target_post.created AS target_date, + reply_post.`uri-id` AS reply_id, + reply_post.`author-id` AS reply_author, + reply_post.created AS reply_date, + like_post.`uri-id` AS like_id, + like_post.`author-id` AS like_author, + like_post.created AS like_date + FROM + post AS original_post + JOIN + post AS target_post + ON + original_post.`uri-id` = target_post.`parent-uri-id` + JOIN + post AS reply_post + ON + target_post.`uri-id` = reply_post.`thr-parent-id` AND + reply_post.`author-id` = ? AND + reply_post.`author-id` != target_post.`author-id` AND + reply_post.`author-id` != original_post.`author-id` AND + reply_post.`uri-id` != reply_post.`thr-parent-id` AND + reply_post.vid = ? AND + reply_post.created > CURDATE() - INTERVAL 1 MONTH + LEFT OUTER JOIN + post AS like_post + ON + reply_post.`uri-id` = like_post.`thr-parent-id` AND + like_post.vid = ? AND + like_post.`author-id` != reply_post.`author-id` + ) AS post_meta +) AS reply_counts +', $contact_uid, $post_vid, $like_vid); + return $reply_guy_result; + } + + // https://stackoverflow.com/a/48283297/235936 + protected function sigFig($value, $digits) + { + if ($value == 0) { + $decimalPlaces = $digits - 1; + } elseif ($value < 0) { + $decimalPlaces = $digits - floor(log10($value * -1)) - 1; + } else { + $decimalPlaces = $digits - floor(log10($value)) - 1; + } + + $answer = ($decimalPlaces > 0) ? + number_format($value, $decimalPlaces) : round($value, $decimalPlaces); + return $answer; + } + + protected function fillReplyGuyData(&$user) { + $reply_guy_result = $this->getReplyGuyRow($user['user_contact_uid']); + if (DBA::isResult($reply_guy_result)) { + $reply_guy_result_row = DBA::fetch($reply_guy_result); + $user['reply_count'] = $reply_guy_result_row['replies_total'] ?? 0; + $user['reply_likes'] = $reply_guy_result_row['like_total'] ?? 0; + $user['reply_respondee_likes'] = $reply_guy_result_row['target_like_total'] ?? 0; + $user['reply_op_likes'] = $reply_guy_result_row['original_like_total'] ?? 0; + + $denominator = $user['reply_likes'] + $user['reply_respondee_likes'] + $user['reply_op_likes']; + if ($user['reply_count'] == 0) { + $user['reply_guy'] = false; + $user['reply_guy_score'] = 0; + } + elseif ($denominator == 0) { + $user['reply_guy'] = true; + $user['reply_guy_score'] = '∞'; + } + else { + $reply_guy_score = $user['reply_count'] / $denominator; + $user['reply_guy'] = $reply_guy_score >= 1.0; + $user['reply_guy_score'] = $this->sigFig($reply_guy_score, 2); + } + } + else { + $user['reply_count'] = "error"; + $user['reply_likes'] = "error"; + $user['reply_respondee_likes'] = "error"; + $user['reply_op_likes'] = "error"; + $user['reply_guy'] = false; + $user['reply_guy_score'] = 0; + } + } + protected function setupUserCallback(): \Closure { Logger::debug("ratioed: setupUserCallback"); @@ -179,6 +314,8 @@ class RatioedPanel extends Active $user['ratioed'] = false; } + $this->fillReplyGuyData($user); + $user = $parentCallback($user); Logger::debug("ratioed: setupUserCallback", [ 'uid' => $user['uid'], diff --git a/ratioed/ratioed.php b/ratioed/ratioed.php index 581ef89b..443fdaa9 100644 --- a/ratioed/ratioed.php +++ b/ratioed/ratioed.php @@ -2,7 +2,7 @@ /** * Name: Ratioed * Description: Additional moderation user table with statistics about user behaviour - * Version: 0.2 + * Version: 0.3 * Author: Matthew Exon */ diff --git a/ratioed/templates/help.tpl b/ratioed/templates/help.tpl index fee47e34..0d012ca7 100644 --- a/ratioed/templates/help.tpl +++ b/ratioed/templates/help.tpl @@ -2,7 +2,7 @@

Ratioed Plugin Help

- This plugin provides administrators with additional statistics about + This plugin provides moderators with additional statistics about the behaviour of users. These may be useful as early warning signs that warrant more carefully watching the behaviour of a user. They are not suitable as a trigger for instantly blocking, @@ -28,7 +28,7 @@

This plugin allows viewing of an actual ratio, calculated over the last 24 hours. This is a useful timeframe for sudden dogpiling - events that administrators might not otherwise notice. The plugin + events that moderators might not otherwise notice. The plugin also calculates other statistics.

Explanation of Statistics

@@ -68,10 +68,63 @@ 24h". It is intended to approximate the traditional ratio as understood on Twitter.

+

Replies last month

+

+ This is the number of times the user posted a reply to someone + else, on a thread the user did not start, any time in the last + month. +

+

Reply likes

+

+ This is the number of likes received by the user on their + replies to other people's posts in the last month. Replies that + receive likes can be assumed to be more of a valuable + contribution than replies that do not. +

+

Respondee likes

+

+ The number of times in the last month the user replied to + someone else's comment and that person then liked the reply. + Likes to replies are not necessarily a positive thing, but if + the person you're replying to approves the reply, that's a very + good sign. Of course it's also common in a debate for neither + side to like the other side's comments without that indicating + an unhealthy interaction, so interpret this statistic cautiously. +

+

OP likes

+

+ The number of times in the last month the user replied on a + thread and the original poster that started the thread liked the + reply. While there is no formal concept of "ownership" of a + thread, conventionally the original poster is assumed to have + started the thread for a reason, and making replies that do not + fulfil that purpose are bad etiquette. Getting approval from + the original poster therefore is a good sign that the user is + posting replies that are wanted. +

+

Reply guy score

+

+ A "reply + guy" is a common Internet phenomenon of people + (disproportionately male) posting unwanted comments on other + (disproportionately female) people's threads, derailing the + conversation. This score loosely quantifies this phenomenon, + as the ratio betwen the number of replies and the sum of likes, + respondee likes, and OP likes. This formula gives extra weight + to particularly relevant likes: a reply to a top-level post that + is liked by the original poster scores the maximum of 3 + "points". A score above 1.0 might indicate cause for concern + for moderators. +

+

+ Since this is indicative of long-term behaviour, the score is + calculated over a month instead of 24 hours. +

+

Performance

The statistics are computed from scratch each time the page loads. - It's possible that this might put a heavy load on the database. and + It's possible that this might put a heavy load on the database, and the page may take a long time to load.

Extending

diff --git a/ratioed/templates/ratioed.tpl b/ratioed/templates/ratioed.tpl index 24ae20b8..2413ed9d 100644 --- a/ratioed/templates/ratioed.tpl +++ b/ratioed/templates/ratioed.tpl @@ -36,7 +36,7 @@ {{foreach $users as $u}} - + {{$u.name}} @@ -121,18 +121,7 @@ {{/foreach}} - - {{if $u.is_deletable}} - - - - - - - {{else}} -   - {{/if}} - + {{/foreach}}