]> git.mxchange.org Git - friendica-addons.git/commitdiff
Ratioed: add statistics about reply likes and reply guy score
authorMatthew Exon <git.mexon@spamgourmet.com>
Sat, 4 Jan 2025 23:23:51 +0000 (00:23 +0100)
committerMatthew Exon <git.mexon@spamgourmet.com>
Sun, 12 Jan 2025 13:33:21 +0000 (14:33 +0100)
ratioed/RatioedPanel.php
ratioed/templates/help.tpl
ratioed/templates/ratioed.tpl

index 19f6dd446ffaf93b0c9916ed4a85051f075f102a..33a009cdee3fdca5a1925190c05a9cf6f16abd0b 100644 (file)
@@ -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'],
index fee47e34b5fdf21ce480672dce2a1910e337fab6..db6480cfff02cdb6fe4214765ce3c5bef24f8e42 100644 (file)
@@ -2,7 +2,7 @@
   <div class="panel-body">
     <h2>Ratioed Plugin Help</h2>
     <p>
-      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 <em>not</em> suitable as a trigger for instantly blocking,
@@ -28,7 +28,7 @@
     <p>
       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.
     </p>
     <h3>Explanation of Statistics</h3>
       24h".  It is intended to approximate the traditional ratio as
       understood on Twitter.
     </p>
+    <h4>Replies last month</h4>
+    <p>
+      This is the number of times the user posted a reply to someone any time in the last month.
+    </p>
+    <h4>Reply likes</h4>
+    <p>
+      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.
+    </p>
+    <h4>Respondee likes</h4>
+    <p>
+      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, but the debate still can
+      be valuable.
+    </p>
+    <h4>OP likes</h4>
+    <p>
+      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.
+    </p>
+    <h4>Reply guy score</h4>
+    <p>
+      A <a href="https://en.wikipedia.org/wiki/Reply_guy">"reply
+      guy"</a> 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 approximates 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.
+    </p>
+    <p>
+      Since this is indicative of long-term behaviour, the score is
+      calculated over a month instead of 24 hours.
+    </p>
+    </p>
     <h3>Performance</h3>
     <p>
       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.
     </p>
     <h3>Extending</h3>
index 24ae20b8e2658e775a8e3a0d24f295354b1ecf9d..6ee6b1d0144ec52be52212fe857608ae944855a2 100644 (file)
@@ -36,7 +36,7 @@
                        </thead>
                        <tbody>
                        {{foreach $users as $u}}
-                               <tr id="user-{{$u.uid}}" class="{{if $u.ratioed}}blocked{{/if}}">
+                               <tr id="user-{{$u.uid}}" class="{{if $u.ratioed || $u.reply_guy}}blocked{{/if}}">
                                        <td></td>
                                        <td><img class="avatar-nano" src="{{$u.micro}}" title="{{$u.nickname}}"></td>
                                        <td><a href="{{$u.url}}" title="{{$u.nickname}}"> {{$u.name}}</a></td>