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
$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',
'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);
]);
}
+ 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");
$user['ratioed'] = false;
}
+ $this->fillReplyGuyData($user);
+
$user = $parentCallback($user);
Logger::debug("ratioed: setupUserCallback", [
'uid' => $user['uid'],
<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,
<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>