track user statistics like post count

This commit is contained in:
rimu 2024-09-13 11:08:04 +12:00
parent 1a658d007f
commit 51f2b3e40e
7 changed files with 116 additions and 24 deletions

View file

@ -1104,6 +1104,7 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
post.delete_dependencies()
announce_activity_to_followers(post.community, post.author, request_json)
post.deleted = True
post.author.post_count -= 1
db.session.commit()
activity_log.result = 'success'
else:
@ -1119,6 +1120,7 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
reply.post.reply_count -= 1
reply.deleted = True
announce_activity_to_followers(reply.community, reply.author, request_json)
reply.author.post_reply_count -= 1
db.session.commit()
activity_log.result = 'success'
else:

View file

@ -543,6 +543,7 @@ def refresh_user_profile_task(user_id):
user.cover = cover
db.session.add(cover)
cover_changed = True
user.recalculate_post_stats()
db.session.commit()
if user.avatar_id and avatar_changed:
make_image_sizes(user.avatar_id, 40, 250, 'users')
@ -1310,11 +1311,14 @@ def downvote_post(post, user):
post.down_votes += 1
# Make 'hot' sort more spicy by amplifying the effect of early downvotes
if post.up_votes + post.down_votes <= 30:
post.score -= current_app.config['SPICY_UNDER_30']
effect = current_app.config['SPICY_UNDER_30']
elif post.up_votes + post.down_votes <= 60:
post.score -= current_app.config['SPICY_UNDER_60']
effect = current_app.config['SPICY_UNDER_60']
else:
post.score -= 1.0
effect = -1.0
if user.cannot_vote():
effect = 0
post.score -= effect
vote = PostVote(user_id=user.id, post_id=post.id, author_id=post.author.id,
effect=effect)
post.author.reputation += effect
@ -1349,7 +1353,9 @@ def downvote_post_reply(comment, user):
if not existing_vote:
effect = -1.0
comment.down_votes += 1
comment.score -= 1.0
if user.cannot_vote():
effect = 0
comment.score -= effect
vote = PostReplyVote(user_id=user.id, post_reply_id=comment.id,
author_id=comment.author.id, effect=effect)
comment.author.reputation += effect
@ -1381,6 +1387,8 @@ def upvote_post_reply(comment, user):
effect = instance_weight(user.ap_domain)
existing_vote = PostReplyVote.query.filter_by(user_id=user.id,
post_reply_id=comment.id).first()
if user.cannot_vote():
effect = 0
if not existing_vote:
comment.up_votes += 1
comment.score += effect
@ -1424,6 +1432,8 @@ def upvote_post(post, user):
spicy_effect = effect * current_app.config['SPICY_UNDER_30']
elif post.up_votes + post.down_votes <= 60:
spicy_effect = effect * current_app.config['SPICY_UNDER_60']
if user.cannot_vote():
effect = spicy_effect = 0
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first()
if not existing_vote:
post.up_votes += 1
@ -1474,6 +1484,7 @@ def delete_post_or_comment_task(user_ap_id, community_ap_id, to_be_deleted_ap_id
to_delete.delete_dependencies()
to_delete.deleted = True
community.post_count -= 1
to_delete.author.post_count -= 1
db.session.commit()
if to_delete.author.id != deletor.id:
add_to_modlog_activitypub('delete_post', deletor, community_id=community.id,
@ -1487,6 +1498,7 @@ def delete_post_or_comment_task(user_ap_id, community_ap_id, to_be_deleted_ap_id
else:
to_delete.delete_dependencies()
to_delete.deleted = True
to_delete.author.post_reply_count -= 1
db.session.commit()
if to_delete.author.id != deletor.id:
add_to_modlog_activitypub('delete_post_reply', deletor, community_id=community.id,
@ -1513,6 +1525,7 @@ def restore_post_or_comment_task(object_json):
# TODO: restore_dependencies()
to_restore.deleted = False
community.post_count += 1
to_restore.author.post_count += 1
db.session.commit()
if to_restore.author.id != restorer.id:
add_to_modlog_activitypub('restore_post', restorer, community_id=community.id,
@ -1816,6 +1829,7 @@ def create_post_reply(activity_log: ActivityPubLog, community: Community, in_rep
community.last_active = post.last_active = utcnow()
activity_log.result = 'success'
post_reply.ranking = confidence(post_reply.up_votes, post_reply.down_votes)
user.post_reply_count += 1
db.session.commit()
# send notification to the post/comment being replied to
@ -2014,6 +2028,7 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
community.post_count += 1
community.last_active = utcnow()
activity_log.result = 'success'
user.post_count += 1
db.session.commit()
# Polls need to be processed quite late because they need a post_id to refer to

View file

@ -584,6 +584,7 @@ def add_post(actor, type):
post = Post(user_id=current_user.id, community_id=form.communities.data, instance_id=1)
save_post(form, post, post_type)
community.post_count += 1
current_user.post_count += 1
community.last_active = g.site.last_active = utcnow()
db.session.commit()
post.ap_id = f"https://{current_app.config['SERVER_NAME']}/post/{post.id}"

View file

@ -637,6 +637,8 @@ class User(UserMixin, db.Model):
timezone = db.Column(db.String(20))
reputation = db.Column(db.Float, default=0.0)
attitude = db.Column(db.Float, default=1.0) # (upvotes cast - downvotes cast) / (upvotes + downvotes). A number between 1 and -1 is the ratio between up and down votes they cast
post_count = db.Column(db.Integer, default=0)
post_reply_count = db.Column(db.Integer, default=0)
stripe_customer_id = db.Column(db.String(50))
stripe_subscription_id = db.Column(db.String(50))
searchable = db.Column(db.Boolean, default=True)
@ -805,6 +807,11 @@ class User(UserMixin, db.Model):
return False
return True
def cannot_vote(self):
if self.is_local():
return False
return self.post_count == 0 and self.post_reply_count == 0 and len(self.user_name) == 8 # most vote manipulation bots have 8 character user names and never post any content
def link(self) -> str:
if self.is_local():
return self.user_name
@ -843,24 +850,21 @@ class User(UserMixin, db.Model):
return self.expires < datetime(2019, 9, 1)
def recalculate_attitude(self):
upvotes = db.session.execute(text('SELECT COUNT(id) as c FROM "post_vote" WHERE user_id = :user_id AND effect > 0'),
{'user_id': self.id}).scalar()
downvotes = db.session.execute(text('SELECT COUNT(id) as c FROM "post_vote" WHERE user_id = :user_id AND effect < 0'),
{'user_id': self.id}).scalar()
if upvotes is None:
upvotes = 0
if downvotes is None:
downvotes = 0
upvotes = downvotes = 0
last_50_votes = PostVote.query.filter(PostVote.user_id == self.id).order_by(-PostVote.id).limit(50)
for vote in last_50_votes:
if vote.effect > 0:
upvotes += 1
if vote.effect < 0:
downvotes += 1
comment_upvotes = db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply_vote" WHERE user_id = :user_id AND effect > 0'),
{'user_id': self.id}).scalar()
comment_downvotes = db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply_vote" WHERE user_id = :user_id AND effect < 0'),
{'user_id': self.id}).scalar()
if comment_upvotes is None:
comment_upvotes = 0
if comment_downvotes is None:
comment_downvotes = 0
comment_upvotes = comment_downvotes = 0
last_50_votes = PostReplyVote.query.filter(PostReplyVote.user_id == self.id).order_by(-PostReplyVote.id).limit(50)
for vote in last_50_votes:
if vote.effect > 0:
comment_upvotes += 1
if vote.effect < 0:
comment_downvotes += 1
total_upvotes = upvotes + comment_upvotes
total_downvotes = downvotes + comment_downvotes
@ -873,6 +877,14 @@ class User(UserMixin, db.Model):
else:
self.attitude = 1.0
def recalculate_post_stats(self, posts=True, replies=True):
if posts:
self.post_count = db.session.execute(text('SELECT COUNT(id) as c FROM "post" WHERE user_id = :user_id AND deleted = false'),
{'user_id': self.id}).scalar()
if replies:
self.post_reply_count = db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE user_id = :user_id AND deleted = false'),
{'user_id': self.id}).scalar()
def subscribed(self, community_id: int) -> int:
if community_id is None:
return False

View file

@ -15,7 +15,7 @@ from app.community.util import save_post, send_to_remote_instance
from app.inoculation import inoculation
from app.post.forms import NewReplyForm, ReportPostForm, MeaCulpaForm
from app.community.forms import CreateLinkForm, CreateImageForm, CreateDiscussionForm, CreateVideoForm, CreatePollForm, EditImageForm
from app.post.util import post_replies, get_comment_branch, post_reply_count, tags_to_string, url_needs_archive, \
from app.post.util import post_replies, get_comment_branch, get_post_reply_count, tags_to_string, url_needs_archive, \
generate_archive_link, body_has_no_archive_link
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR, POST_TYPE_LINK, \
POST_TYPE_IMAGE, \
@ -111,6 +111,7 @@ def show_post(post_id: int):
post.last_active = community.last_active = utcnow()
post.reply_count += 1
community.post_reply_count += 1
current_user.post_reply_count += 1
current_user.language_id = form.language_id.data
db.session.add(reply)
@ -737,8 +738,9 @@ def add_reply(post_id: int, comment_id: int):
elif current_user.reputation < -100:
reply.score -= 1
reply.ranking -= 1
post.reply_count = post_reply_count(post.id)
post.reply_count = get_post_reply_count(post.id)
post.last_active = post.community.last_active = utcnow()
current_user.post_reply_count += 1
db.session.commit()
form.body.data = ''
flash('Your comment has been added.')
@ -1201,6 +1203,8 @@ def post_delete_post(community: Community, post: Post, user_id: int, federate_al
ocp.cross_posts.remove(post.id)
post.delete_dependencies()
post.deleted = True
post.author.post_count -= 1
community.post_count -= 1
if hasattr(g, 'site'): # g.site is invalid when running from cli
g.site.last_active = community.last_active = utcnow()
flash(_('Post deleted.'))
@ -1266,6 +1270,8 @@ def post_restore(post_id: int):
post = Post.query.get_or_404(post_id)
if post.community.is_moderator() or post.community.is_owner() or current_user.is_admin():
post.deleted = False
post.author.post_count += 1
post.community.post_count += 1
db.session.commit()
# Federate un-delete
@ -1781,6 +1787,7 @@ def post_reply_delete(post_id: int, comment_id: int):
post_reply.delete_dependencies()
post_reply.deleted = True
g.site.last_active = community.last_active = utcnow()
post_reply.author.post_reply_count -= 1
db.session.commit()
flash(_('Comment deleted.'))
# federate delete

View file

@ -76,7 +76,7 @@ def get_comment_branch(post_id: int, comment_id: int, sort_by: str) -> List[Post
# The number of replies a post has
def post_reply_count(post_id) -> int:
def get_post_reply_count(post_id) -> int:
return db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE post_id = :post_id AND deleted is false'),
{'post_id': post_id}).scalar()

View file

@ -0,0 +1,55 @@
"""user stats
Revision ID: fdaeb0b2c078
Revises: 2cae414cbc7a
Create Date: 2024-09-13 09:37:24.847306
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'fdaeb0b2c078'
down_revision = '2cae414cbc7a'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('post_count', sa.Integer(), nullable=True))
batch_op.add_column(sa.Column('post_reply_count', sa.Integer(), nullable=True))
# ### end Alembic commands ###
op.execute(sa.DDL('UPDATE "user" SET post_count = 0, post_reply_count = 0'))
op.execute(sa.DDL("""UPDATE "user"
SET post_count = subquery.post_count
FROM (
SELECT user_id, COUNT(*) as post_count
FROM post
WHERE deleted = false
GROUP BY user_id
) AS subquery
WHERE "user".id = subquery.user_id;"""))
op.execute(sa.DDL("""UPDATE "user"
SET post_reply_count = subquery.post_count
FROM (
SELECT user_id, COUNT(*) as post_count
FROM post_reply
WHERE deleted = false
GROUP BY user_id
) AS subquery
WHERE "user".id = subquery.user_id;"""))
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_column('post_reply_count')
batch_op.drop_column('post_count')
# ### end Alembic commands ###