mirror of
https://git.friendi.ca/friendica/friendica-addons.git
synced 2025-01-22 04:39:46 +00:00
Merge pull request 'Ratioed: add statistics about reply likes and reply guy score' (#1589) from mexon/friendica-addons:mat/reply-guy-score into develop
Reviewed-on: https://git.friendi.ca/friendica/friendica-addons/pulls/1589
This commit is contained in:
commit
0c96d0f4bb
4 changed files with 196 additions and 17 deletions
|
@ -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'],
|
||||
|
|
|
@ -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 <http://mat.exon.name>
|
||||
*/
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
@ -68,10 +68,63 @@
|
|||
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
|
||||
else, on a thread the user did not start, 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 without that indicating
|
||||
an unhealthy interaction, so interpret this statistic cautiously.
|
||||
</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 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.
|
||||
</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>
|
||||
|
|
|
@ -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>
|
||||
|
@ -121,18 +121,7 @@
|
|||
{{/foreach}}
|
||||
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{if $u.is_deletable}}
|
||||
<a href="{{$baseurl}}/moderation/users/active/block/{{$u.uid}}?t={{$form_security_token}}" class="admin-settings-action-link" title="{{$block}}">
|
||||
<i class="fa fa-ban" aria-hidden="true"></i>
|
||||
</a>
|
||||
<a href="{{$baseurl}}/moderation/users/active/delete/{{$u.uid}}?t={{$form_security_token}}" class="admin-settings-action-link" title="{{$delete}}" onclick="return confirm_delete('{{$confirm_delete}}','{{$u.name}}')">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</a>
|
||||
{{else}}
|
||||
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="text-right"></td>
|
||||
</tr>
|
||||
{{/foreach}}
|
||||
</tbody>
|
||||
|
|
Loading…
Add table
Reference in a new issue