mirror of
https://git.friendi.ca/friendica/friendica-addons.git
synced 2025-01-22 07:59:47 +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\Database\DBA;
|
||||||
use Friendica\DI;
|
use Friendica\DI;
|
||||||
use Friendica\Model\User;
|
use Friendica\Model\User;
|
||||||
|
use Friendica\Model\Verb;
|
||||||
use Friendica\Module\Moderation\Users\Active;
|
use Friendica\Module\Moderation\Users\Active;
|
||||||
|
use Friendica\Protocol\Activity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class implements the "Behaviour" panel in Moderation/Users
|
* This class implements the "Behaviour" panel in Moderation/Users
|
||||||
|
@ -75,6 +77,11 @@ class RatioedPanel extends Active
|
||||||
$this->t('Comments last 24h'),
|
$this->t('Comments last 24h'),
|
||||||
$this->t('Reactions last 24h'),
|
$this->t('Reactions last 24h'),
|
||||||
$this->t('Ratio 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 = [
|
$field_names = [
|
||||||
'name',
|
'name',
|
||||||
|
@ -87,6 +94,11 @@ class RatioedPanel extends Active
|
||||||
'comments',
|
'comments',
|
||||||
'reactions',
|
'reactions',
|
||||||
'ratio',
|
'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);
|
$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
|
protected function setupUserCallback(): \Closure
|
||||||
{
|
{
|
||||||
Logger::debug("ratioed: setupUserCallback");
|
Logger::debug("ratioed: setupUserCallback");
|
||||||
|
@ -179,6 +314,8 @@ class RatioedPanel extends Active
|
||||||
$user['ratioed'] = false;
|
$user['ratioed'] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->fillReplyGuyData($user);
|
||||||
|
|
||||||
$user = $parentCallback($user);
|
$user = $parentCallback($user);
|
||||||
Logger::debug("ratioed: setupUserCallback", [
|
Logger::debug("ratioed: setupUserCallback", [
|
||||||
'uid' => $user['uid'],
|
'uid' => $user['uid'],
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
/**
|
/**
|
||||||
* Name: Ratioed
|
* Name: Ratioed
|
||||||
* Description: Additional moderation user table with statistics about user behaviour
|
* Description: Additional moderation user table with statistics about user behaviour
|
||||||
* Version: 0.2
|
* Version: 0.3
|
||||||
* Author: Matthew Exon <http://mat.exon.name>
|
* Author: Matthew Exon <http://mat.exon.name>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<h2>Ratioed Plugin Help</h2>
|
<h2>Ratioed Plugin Help</h2>
|
||||||
<p>
|
<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
|
the behaviour of users. These may be useful as early warning signs
|
||||||
that warrant more carefully watching the behaviour of a user. They
|
that warrant more carefully watching the behaviour of a user. They
|
||||||
are <em>not</em> suitable as a trigger for instantly blocking,
|
are <em>not</em> suitable as a trigger for instantly blocking,
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
<p>
|
<p>
|
||||||
This plugin allows viewing of an actual ratio, calculated over the
|
This plugin allows viewing of an actual ratio, calculated over the
|
||||||
last 24 hours. This is a useful timeframe for sudden dogpiling
|
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.
|
also calculates other statistics.
|
||||||
</p>
|
</p>
|
||||||
<h3>Explanation of Statistics</h3>
|
<h3>Explanation of Statistics</h3>
|
||||||
|
@ -68,10 +68,63 @@
|
||||||
24h". It is intended to approximate the traditional ratio as
|
24h". It is intended to approximate the traditional ratio as
|
||||||
understood on Twitter.
|
understood on Twitter.
|
||||||
</p>
|
</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>
|
<h3>Performance</h3>
|
||||||
<p>
|
<p>
|
||||||
The statistics are computed from scratch each time the page loads.
|
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.
|
the page may take a long time to load.
|
||||||
</p>
|
</p>
|
||||||
<h3>Extending</h3>
|
<h3>Extending</h3>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{foreach $users as $u}}
|
{{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></td>
|
||||||
<td><img class="avatar-nano" src="{{$u.micro}}" title="{{$u.nickname}}"></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>
|
<td><a href="{{$u.url}}" title="{{$u.nickname}}"> {{$u.name}}</a></td>
|
||||||
|
@ -121,18 +121,7 @@
|
||||||
{{/foreach}}
|
{{/foreach}}
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right"></td>
|
||||||
{{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>
|
|
||||||
</tr>
|
</tr>
|
||||||
{{/foreach}}
|
{{/foreach}}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
Loading…
Add table
Reference in a new issue