2024-10-14 15:37:00 +13:00
import html
2023-12-12 08:53:35 +13:00
from datetime import datetime , timedelta , date , timezone
2023-08-05 21:26:24 +12:00
from time import time
2024-10-14 15:37:00 +13:00
from typing import List , Union , Type
2024-09-07 11:33:20 +12:00
from urllib . parse import urlparse , parse_qs , urlencode , urlunparse
2023-09-17 21:19:51 +12:00
2024-09-30 16:34:17 +13:00
import arrow
2024-02-19 15:01:53 +13:00
from flask import current_app , escape , url_for , render_template_string
2023-11-30 06:36:08 +13:00
from flask_login import UserMixin , current_user
2024-05-21 22:20:08 +12:00
from sqlalchemy import or_ , text , desc
2023-08-05 21:26:24 +12:00
from werkzeug . security import generate_password_hash , check_password_hash
from flask_babel import _ , lazy_gettext as _l
from sqlalchemy . orm import backref
from sqlalchemy_utils . types import TSVectorType # https://sqlalchemy-searchable.readthedocs.io/en/latest/installation.html
2024-03-31 02:15:10 +01:00
from sqlalchemy . dialects . postgresql import ARRAY
from sqlalchemy . ext . mutable import MutableList
2023-10-03 22:29:13 +13:00
from flask_sqlalchemy import BaseQuery
from sqlalchemy_searchable import SearchQueryMixin
2024-10-14 15:37:00 +13:00
from app import db , login , cache , celery , httpx_client , constants
2023-08-05 21:26:24 +12:00
import jwt
2023-11-30 20:57:51 +13:00
import os
2024-05-18 19:41:20 +12:00
import math
2023-08-05 21:26:24 +12:00
2023-09-08 20:04:01 +12:00
from app . constants import SUBSCRIPTION_NONMEMBER , SUBSCRIPTION_MEMBER , SUBSCRIPTION_MODERATOR , SUBSCRIPTION_OWNER , \
2024-06-27 15:19:32 +08:00
SUBSCRIPTION_BANNED , SUBSCRIPTION_PENDING , NOTIF_USER , NOTIF_COMMUNITY , NOTIF_TOPIC , NOTIF_POST , NOTIF_REPLY , \
ROLE_ADMIN , ROLE_STAFF
2023-08-29 22:01:06 +12:00
2023-08-05 21:26:24 +12:00
2023-12-17 20:33:27 +13:00
# datetime.utcnow() is depreciated in Python 3.12 so it will need to be swapped out eventually
2023-12-12 08:53:35 +13:00
def utcnow ( ) :
2023-12-17 20:33:27 +13:00
return datetime . utcnow ( )
2023-12-12 08:53:35 +13:00
2023-10-03 22:29:13 +13:00
class FullTextSearchQuery ( BaseQuery , SearchQueryMixin ) :
pass
2024-01-03 16:29:58 +13:00
class BannedInstances ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
domain = db . Column ( db . String ( 256 ) , index = True )
reason = db . Column ( db . String ( 256 ) )
initiator = db . Column ( db . String ( 256 ) )
created_at = db . Column ( db . DateTime , default = utcnow )
class AllowedInstances ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
domain = db . Column ( db . String ( 256 ) , index = True )
created_at = db . Column ( db . DateTime , default = utcnow )
class Instance ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
domain = db . Column ( db . String ( 256 ) , index = True )
inbox = db . Column ( db . String ( 256 ) )
shared_inbox = db . Column ( db . String ( 256 ) )
outbox = db . Column ( db . String ( 256 ) )
vote_weight = db . Column ( db . Float , default = 1.0 )
software = db . Column ( db . String ( 50 ) )
version = db . Column ( db . String ( 50 ) )
created_at = db . Column ( db . DateTime , default = utcnow )
updated_at = db . Column ( db . DateTime , default = utcnow )
last_seen = db . Column ( db . DateTime , default = utcnow ) # When an Activity was received from them
last_successful_send = db . Column ( db . DateTime ) # When we successfully sent them an Activity
failures = db . Column ( db . Integer , default = 0 ) # How many times we failed to send (reset to 0 after every successful send)
most_recent_attempt = db . Column ( db . DateTime ) # When the most recent failure was
2024-01-04 16:09:22 +13:00
dormant = db . Column ( db . Boolean , default = False ) # True once this instance is considered offline and not worth sending to any more
2024-01-03 16:29:58 +13:00
start_trying_again = db . Column ( db . DateTime ) # When to start trying again. Should grow exponentially with each failure.
2024-01-04 16:09:22 +13:00
gone_forever = db . Column ( db . Boolean , default = False ) # True once this instance is considered offline forever - never start trying again
2024-01-03 16:29:58 +13:00
ip_address = db . Column ( db . String ( 50 ) )
2024-02-23 16:52:17 +13:00
trusted = db . Column ( db . Boolean , default = False )
2024-04-18 20:51:08 +12:00
posting_warning = db . Column ( db . String ( 512 ) )
2024-05-23 14:19:23 +01:00
nodeinfo_href = db . Column ( db . String ( 100 ) )
2024-01-03 16:29:58 +13:00
posts = db . relationship ( ' Post ' , backref = ' instance ' , lazy = ' dynamic ' )
post_replies = db . relationship ( ' PostReply ' , backref = ' instance ' , lazy = ' dynamic ' )
communities = db . relationship ( ' Community ' , backref = ' instance ' , lazy = ' dynamic ' )
def online ( self ) :
2024-08-19 10:18:23 +12:00
return not ( self . dormant or self . gone_forever )
2024-01-03 16:29:58 +13:00
2024-02-14 12:31:44 +13:00
def user_is_admin ( self , user_id ) :
role = InstanceRole . query . filter_by ( instance_id = self . id , user_id = user_id ) . first ( )
return role and role . role == ' admin '
2024-08-20 07:03:08 +12:00
def votes_are_public ( self ) :
2024-09-24 12:04:29 +12:00
if self . trusted is True : # only vote privately with untrusted instances
return False
2024-10-10 15:39:36 +01:00
return self . software . lower ( ) == ' lemmy ' or self . software . lower ( ) == ' mbin ' or self . software . lower ( ) == ' kbin ' or self . software . lower ( ) == ' guppe groups '
2024-08-20 07:03:08 +12:00
2024-09-05 11:59:01 -04:00
def post_count ( self ) :
2024-09-06 16:00:09 +12:00
return db . session . execute ( text ( ' SELECT COUNT(id) as c FROM " post " WHERE instance_id = :instance_id ' ) ,
{ ' instance_id ' : self . id } ) . scalar ( )
2024-09-05 11:59:01 -04:00
def post_replies_count ( self ) :
2024-09-06 16:00:09 +12:00
return db . session . execute ( text ( ' SELECT COUNT(id) as c FROM " post_reply " WHERE instance_id = :instance_id ' ) ,
{ ' instance_id ' : self . id } ) . scalar ( )
2024-09-05 11:59:01 -04:00
def known_communities_count ( self ) :
2024-09-06 16:00:09 +12:00
return db . session . execute ( text ( ' SELECT COUNT(id) as c FROM " community " WHERE instance_id = :instance_id ' ) ,
{ ' instance_id ' : self . id } ) . scalar ( )
2024-09-05 11:59:01 -04:00
2024-09-05 13:24:30 -04:00
def known_users_count ( self ) :
2024-09-06 16:00:09 +12:00
return db . session . execute ( text ( ' SELECT COUNT(id) as c FROM " user " WHERE instance_id = :instance_id ' ) ,
{ ' instance_id ' : self . id } ) . scalar ( )
2024-09-05 11:59:01 -04:00
2024-10-23 08:37:08 +13:00
def update_dormant_gone ( self ) :
if self . failures > 7 and self . dormant == True :
self . gone_forever = True
elif self . failures > 2 and self . dormant == False :
self . dormant = True
2024-09-13 16:39:42 +12:00
@classmethod
def weight ( cls , domain : str ) :
if domain :
instance = Instance . query . filter_by ( domain = domain ) . first ( )
if instance :
return instance . vote_weight
return 1.0
2024-08-18 13:12:58 +12:00
def __repr__ ( self ) :
return ' <Instance {} > ' . format ( self . domain )
2024-09-03 16:47:26 +00:00
@classmethod
def unique_software_names ( cls ) :
return list ( db . session . execute ( text ( ' SELECT DISTINCT software FROM instance ORDER BY software ' ) ) . scalars ( ) )
2024-02-14 12:31:44 +13:00
class InstanceRole ( db . Model ) :
instance_id = db . Column ( db . Integer , db . ForeignKey ( ' instance.id ' ) , primary_key = True )
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , primary_key = True )
role = db . Column ( db . String ( 50 ) , default = ' admin ' )
user = db . relationship ( ' User ' , lazy = ' joined ' )
2024-01-03 16:29:58 +13:00
class InstanceBlock ( db . Model ) :
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , primary_key = True )
instance_id = db . Column ( db . Integer , db . ForeignKey ( ' instance.id ' ) , primary_key = True )
created_at = db . Column ( db . DateTime , default = utcnow )
2024-02-19 15:01:53 +13:00
class Conversation ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
reported = db . Column ( db . Boolean , default = False )
read = db . Column ( db . Boolean , default = False )
created_at = db . Column ( db . DateTime , default = utcnow )
updated_at = db . Column ( db . DateTime , default = utcnow )
initiator = db . relationship ( ' User ' , backref = db . backref ( ' conversations_initiated ' , lazy = ' dynamic ' ) ,
foreign_keys = [ user_id ] )
messages = db . relationship ( ' ChatMessage ' , backref = db . backref ( ' conversation ' ) , cascade = ' all,delete ' ,
lazy = ' dynamic ' )
def member_names ( self , user_id ) :
retval = [ ]
for member in self . members :
if member . id != user_id :
retval . append ( member . display_name ( ) )
return ' , ' . join ( retval )
def is_member ( self , user ) :
for member in self . members :
if member . id == user . id :
return True
return False
def instances ( self ) :
retval = [ ]
for member in self . members :
if member . instance . id != 1 and member . instance not in retval :
retval . append ( member . instance )
return retval
2024-02-19 15:56:56 +13:00
@staticmethod
def find_existing_conversation ( recipient , sender ) :
sql = """ SELECT
c . id AS conversation_id ,
c . created_at AS conversation_created_at ,
c . updated_at AS conversation_updated_at ,
cm1 . user_id AS user1_id ,
cm2 . user_id AS user2_id
FROM
public . conversation AS c
JOIN
public . conversation_member AS cm1 ON c . id = cm1 . conversation_id
JOIN
public . conversation_member AS cm2 ON c . id = cm2 . conversation_id
WHERE
cm1 . user_id = : user_id_1 AND
cm2 . user_id = : user_id_2 AND
cm1 . user_id < > cm2 . user_id ; """
ec = db . session . execute ( text ( sql ) , { ' user_id_1 ' : recipient . id , ' user_id_2 ' : sender . id } ) . fetchone ( )
return Conversation . query . get ( ec [ 0 ] ) if ec else None
2024-02-19 15:01:53 +13:00
conversation_member = db . Table ( ' conversation_member ' ,
db . Column ( ' user_id ' , db . Integer , db . ForeignKey ( ' user.id ' ) ) ,
db . Column ( ' conversation_id ' , db . Integer , db . ForeignKey ( ' conversation.id ' ) ) ,
db . PrimaryKeyConstraint ( ' user_id ' , ' conversation_id ' )
)
class ChatMessage ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
sender_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
recipient_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
conversation_id = db . Column ( db . Integer , db . ForeignKey ( ' conversation.id ' ) , index = True )
body = db . Column ( db . Text )
body_html = db . Column ( db . Text )
reported = db . Column ( db . Boolean , default = False )
read = db . Column ( db . Boolean , default = False )
encrypted = db . Column ( db . String ( 15 ) )
created_at = db . Column ( db . DateTime , default = utcnow )
sender = db . relationship ( ' User ' , foreign_keys = [ sender_id ] )
2024-04-16 21:23:19 +12:00
class Tag ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
2024-05-11 13:45:04 +12:00
name = db . Column ( db . String ( 256 ) , index = True ) # lowercase version of tag, e.g. solarstorm
display_as = db . Column ( db . String ( 256 ) ) # Version of tag with uppercase letters, e.g. SolarStorm
2024-05-12 13:02:45 +12:00
post_count = db . Column ( db . Integer , default = 0 )
banned = db . Column ( db . Boolean , default = False , index = True )
2024-04-16 21:23:19 +12:00
2024-11-02 15:14:31 +13:00
class Licence ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
name = db . Column ( db . String ( 50 ) )
2024-04-16 21:23:19 +12:00
class Language ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
code = db . Column ( db . String ( 5 ) , index = True )
name = db . Column ( db . String ( 50 ) )
community_language = db . Table ( ' community_language ' , db . Column ( ' community_id ' , db . Integer , db . ForeignKey ( ' community.id ' ) ) ,
db . Column ( ' language_id ' , db . Integer , db . ForeignKey ( ' language.id ' ) ) ,
db . PrimaryKeyConstraint ( ' community_id ' , ' language_id ' )
)
post_tag = db . Table ( ' post_tag ' , db . Column ( ' post_id ' , db . Integer , db . ForeignKey ( ' post.id ' ) ) ,
db . Column ( ' tag_id ' , db . Integer , db . ForeignKey ( ' tag.id ' ) ) ,
db . PrimaryKeyConstraint ( ' post_id ' , ' tag_id ' )
)
2023-08-05 21:26:24 +12:00
class File ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
file_path = db . Column ( db . String ( 255 ) )
file_name = db . Column ( db . String ( 255 ) )
width = db . Column ( db . Integer )
height = db . Column ( db . Integer )
2024-05-25 22:44:43 +12:00
alt_text = db . Column ( db . String ( 1500 ) )
2024-02-13 06:50:25 +13:00
source_url = db . Column ( db . String ( 1024 ) )
2023-11-27 22:05:35 +13:00
thumbnail_path = db . Column ( db . String ( 255 ) )
thumbnail_width = db . Column ( db . Integer )
thumbnail_height = db . Column ( db . Integer )
2024-05-06 15:42:12 +12:00
def view_url ( self , resize = False ) :
2023-11-27 22:05:35 +13:00
if self . source_url :
2024-05-06 15:42:12 +12:00
if resize and ' /pictrs/ ' in self . source_url and ' ? ' not in self . source_url :
return f ' { self . source_url } ?thumbnail=1024 '
else :
return self . source_url
2023-11-27 22:05:35 +13:00
elif self . file_path :
file_path = self . file_path [ 4 : ] if self . file_path . startswith ( ' app/ ' ) else self . file_path
2024-11-17 19:45:01 +00:00
scheme = ' http ' if current_app . config [ ' SERVER_NAME ' ] == ' 127.0.0.1:5000 ' else ' https '
return f " { scheme } :// { current_app . config [ ' SERVER_NAME ' ] } / { file_path } "
2023-11-27 22:05:35 +13:00
else :
return ' '
2024-01-25 21:18:44 +13:00
def medium_url ( self ) :
if self . file_path is None :
return self . thumbnail_url ( )
file_path = self . file_path [ 4 : ] if self . file_path . startswith ( ' app/ ' ) else self . file_path
2024-11-17 19:45:01 +00:00
scheme = ' http ' if current_app . config [ ' SERVER_NAME ' ] == ' 127.0.0.1:5000 ' else ' https '
return f " { scheme } :// { current_app . config [ ' SERVER_NAME ' ] } / { file_path } "
2024-01-25 21:18:44 +13:00
2023-11-27 22:05:35 +13:00
def thumbnail_url ( self ) :
2023-12-21 22:14:43 +13:00
if self . thumbnail_path is None :
if self . source_url :
return self . source_url
else :
return ' '
2023-11-27 22:05:35 +13:00
thumbnail_path = self . thumbnail_path [ 4 : ] if self . thumbnail_path . startswith ( ' app/ ' ) else self . thumbnail_path
2024-11-17 19:45:01 +00:00
scheme = ' http ' if current_app . config [ ' SERVER_NAME ' ] == ' 127.0.0.1:5000 ' else ' https '
return f " { scheme } :// { current_app . config [ ' SERVER_NAME ' ] } / { thumbnail_path } "
2023-08-05 21:26:24 +12:00
2023-11-30 20:57:51 +13:00
def delete_from_disk ( self ) :
2024-03-23 15:12:51 +13:00
purge_from_cache = [ ]
2023-11-30 20:57:51 +13:00
if self . file_path and os . path . isfile ( self . file_path ) :
2024-04-07 09:39:50 +12:00
try :
os . unlink ( self . file_path )
except FileNotFoundError as e :
. . .
2024-03-23 15:12:51 +13:00
purge_from_cache . append ( self . file_path . replace ( ' app/ ' , f " https:// { current_app . config [ ' SERVER_NAME ' ] } / " ) )
2023-11-30 20:57:51 +13:00
if self . thumbnail_path and os . path . isfile ( self . thumbnail_path ) :
2024-04-07 09:39:50 +12:00
try :
os . unlink ( self . thumbnail_path )
except FileNotFoundError as e :
. . .
2024-03-23 15:12:51 +13:00
purge_from_cache . append ( self . thumbnail_path . replace ( ' app/ ' , f " https:// { current_app . config [ ' SERVER_NAME ' ] } / " ) )
2024-04-03 20:13:05 +13:00
if self . source_url and self . source_url . startswith ( ' http ' ) and current_app . config [ ' SERVER_NAME ' ] in self . source_url :
# self.source_url is always a url rather than a file path, which makes deleting the file a bit fiddly
2024-04-07 09:39:50 +12:00
try :
os . unlink ( self . source_url . replace ( f " https:// { current_app . config [ ' SERVER_NAME ' ] } / " , ' app/ ' ) )
except FileNotFoundError as e :
. . .
2024-04-03 20:13:05 +13:00
purge_from_cache . append ( self . source_url ) # otoh it makes purging the cdn cache super easy.
2024-03-23 15:12:51 +13:00
if purge_from_cache :
flush_cdn_cache ( purge_from_cache )
2023-11-30 20:57:51 +13:00
2024-02-10 11:42:18 +13:00
def filesize ( self ) :
size = 0
if self . file_path and os . path . exists ( self . file_path ) :
size + = os . path . getsize ( self . file_path )
if self . thumbnail_path and os . path . exists ( self . thumbnail_path ) :
size + = os . path . getsize ( self . thumbnail_path )
return size
2023-08-05 21:26:24 +12:00
2024-03-23 15:12:51 +13:00
def flush_cdn_cache ( url : Union [ str , List [ str ] ] ) :
zone_id = current_app . config [ ' CLOUDFLARE_ZONE_ID ' ]
token = current_app . config [ ' CLOUDFLARE_API_TOKEN ' ]
if zone_id and token :
if current_app . debug :
flush_cdn_cache_task ( url )
else :
flush_cdn_cache_task . delay ( url )
@celery.task
def flush_cdn_cache_task ( to_purge : Union [ str , List [ str ] ] ) :
zone_id = current_app . config [ ' CLOUDFLARE_ZONE_ID ' ]
token = current_app . config [ ' CLOUDFLARE_API_TOKEN ' ]
headers = {
' Authorization ' : f " Bearer { token } " ,
' Content-Type ' : ' application/json '
}
# url can be a string or a list of strings
body = ' '
if isinstance ( to_purge , str ) and to_purge == ' all ' :
body = {
' purge_everything ' : True
}
else :
if isinstance ( to_purge , str ) :
body = {
' files ' : [ to_purge ]
}
elif isinstance ( to_purge , list ) :
body = {
' files ' : to_purge
}
if body :
2024-09-15 19:30:45 +12:00
response = httpx_client . request (
2024-03-23 15:12:51 +13:00
' POST ' ,
f ' https://api.cloudflare.com/client/v4/zones/ { zone_id } /purge_cache ' ,
headers = headers ,
json = body ,
timeout = 5 ,
)
2024-01-04 16:00:19 +13:00
class Topic ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
2024-01-28 18:11:32 +13:00
machine_name = db . Column ( db . String ( 50 ) , index = True )
2024-01-04 16:00:19 +13:00
name = db . Column ( db . String ( 50 ) )
num_communities = db . Column ( db . Integer , default = 0 )
2024-03-01 20:32:29 +13:00
parent_id = db . Column ( db . Integer )
2024-11-08 15:09:24 +13:00
show_posts_in_children = db . Column ( db . Boolean , default = False )
2024-01-04 16:00:19 +13:00
communities = db . relationship ( ' Community ' , lazy = ' dynamic ' , backref = ' topic ' , cascade = " all, delete-orphan " )
2024-04-08 20:01:08 +12:00
def path ( self ) :
return_value = [ self . machine_name ]
parent_id = self . parent_id
while parent_id is not None :
parent_topic = Topic . query . get ( parent_id )
if parent_topic is None :
break
return_value . append ( parent_topic . machine_name )
parent_id = parent_topic . parent_id
return_value = list ( reversed ( return_value ) )
return ' / ' . join ( return_value )
2024-04-29 16:03:00 +12:00
def notify_new_posts ( self , user_id : int ) - > bool :
existing_notification = NotificationSubscription . query . filter ( NotificationSubscription . entity_id == self . id ,
NotificationSubscription . user_id == user_id ,
NotificationSubscription . type == NOTIF_TOPIC ) . first ( )
return existing_notification is not None
2023-08-05 21:26:24 +12:00
class Community ( db . Model ) :
2023-10-03 22:29:13 +13:00
query_class = FullTextSearchQuery
2023-08-05 21:26:24 +12:00
id = db . Column ( db . Integer , primary_key = True )
icon_id = db . Column ( db . Integer , db . ForeignKey ( ' file.id ' ) )
2023-08-29 22:01:06 +12:00
image_id = db . Column ( db . Integer , db . ForeignKey ( ' file.id ' ) )
2023-10-21 15:49:01 +13:00
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) )
2023-08-05 21:26:24 +12:00
name = db . Column ( db . String ( 256 ) , index = True )
title = db . Column ( db . String ( 256 ) )
2023-12-21 22:14:43 +13:00
description = db . Column ( db . Text ) # markdown
description_html = db . Column ( db . Text ) # html equivalent of above markdown
2023-08-05 21:26:24 +12:00
rules = db . Column ( db . Text )
2023-12-21 22:14:43 +13:00
rules_html = db . Column ( db . Text )
2023-12-13 21:04:11 +13:00
content_warning = db . Column ( db . Text ) # "Are you sure you want to view this community?"
2023-08-05 21:26:24 +12:00
subscriptions_count = db . Column ( db . Integer , default = 0 )
post_count = db . Column ( db . Integer , default = 0 )
post_reply_count = db . Column ( db . Integer , default = 0 )
nsfw = db . Column ( db . Boolean , default = False )
nsfl = db . Column ( db . Boolean , default = False )
2023-12-13 21:04:11 +13:00
instance_id = db . Column ( db . Integer , db . ForeignKey ( ' instance.id ' ) , index = True )
low_quality = db . Column ( db . Boolean , default = False ) # upvotes earned in low quality communities don't improve reputation
2023-12-12 08:53:35 +13:00
created_at = db . Column ( db . DateTime , default = utcnow )
last_active = db . Column ( db . DateTime , default = utcnow )
2023-08-05 21:26:24 +12:00
public_key = db . Column ( db . Text )
private_key = db . Column ( db . Text )
2023-12-31 12:09:20 +13:00
content_retention = db . Column ( db . Integer , default = - 1 )
2024-01-04 16:00:19 +13:00
topic_id = db . Column ( db . Integer , db . ForeignKey ( ' topic.id ' ) , index = True )
2024-01-21 15:44:13 +13:00
default_layout = db . Column ( db . String ( 15 ) )
2024-04-18 20:51:08 +12:00
posting_warning = db . Column ( db . String ( 512 ) )
2023-08-05 21:26:24 +12:00
ap_id = db . Column ( db . String ( 255 ) , index = True )
2023-08-22 21:24:11 +12:00
ap_profile_id = db . Column ( db . String ( 255 ) , index = True )
2023-08-05 21:26:24 +12:00
ap_followers_url = db . Column ( db . String ( 255 ) )
ap_preferred_username = db . Column ( db . String ( 255 ) )
ap_discoverable = db . Column ( db . Boolean , default = False )
ap_public_url = db . Column ( db . String ( 255 ) )
ap_fetched_at = db . Column ( db . DateTime )
ap_deleted_at = db . Column ( db . DateTime )
ap_inbox_url = db . Column ( db . String ( 255 ) )
2024-02-23 16:52:17 +13:00
ap_outbox_url = db . Column ( db . String ( 255 ) )
2024-03-19 07:34:19 +00:00
ap_featured_url = db . Column ( db . String ( 255 ) )
2023-12-03 22:41:15 +13:00
ap_moderators_url = db . Column ( db . String ( 255 ) )
2023-08-05 21:26:24 +12:00
ap_domain = db . Column ( db . String ( 255 ) )
banned = db . Column ( db . Boolean , default = False )
2023-08-10 21:13:37 +12:00
restricted_to_mods = db . Column ( db . Boolean , default = False )
2024-01-02 19:41:00 +13:00
local_only = db . Column ( db . Boolean , default = False ) # only users on this instance can post
2023-12-13 21:04:11 +13:00
new_mods_wanted = db . Column ( db . Boolean , default = False )
2023-08-05 21:26:24 +12:00
searchable = db . Column ( db . Boolean , default = True )
2023-09-05 20:25:02 +12:00
private_mods = db . Column ( db . Boolean , default = False )
2023-08-05 21:26:24 +12:00
2023-12-31 12:09:20 +13:00
# Which feeds posts from this community show up in
show_home = db . Column ( db . Boolean , default = False ) # For anonymous users. When logged in, the home feed shows posts from subscribed communities
show_popular = db . Column ( db . Boolean , default = True )
show_all = db . Column ( db . Boolean , default = True )
2024-05-08 21:07:22 +12:00
ignore_remote_language = db . Column ( db . Boolean , default = False )
2023-08-29 22:01:06 +12:00
search_vector = db . Column ( TSVectorType ( ' name ' , ' title ' , ' description ' , ' rules ' ) )
2023-08-05 21:26:24 +12:00
2024-01-02 19:41:00 +13:00
posts = db . relationship ( ' Post ' , lazy = ' dynamic ' , cascade = " all, delete-orphan " )
replies = db . relationship ( ' PostReply ' , lazy = ' dynamic ' , cascade = " all, delete-orphan " )
2024-07-17 22:11:31 +08:00
wiki_pages = db . relationship ( ' CommunityWikiPage ' , lazy = ' dynamic ' , backref = ' community ' , cascade = " all, delete-orphan " )
2023-08-29 22:01:06 +12:00
icon = db . relationship ( ' File ' , foreign_keys = [ icon_id ] , single_parent = True , backref = ' community ' , cascade = " all, delete-orphan " )
image = db . relationship ( ' File ' , foreign_keys = [ image_id ] , single_parent = True , cascade = " all, delete-orphan " )
2024-04-16 21:23:19 +12:00
languages = db . relationship ( ' Language ' , lazy = ' dynamic ' , secondary = community_language , backref = db . backref ( ' communities ' , lazy = ' dynamic ' ) )
2023-08-29 22:01:06 +12:00
2024-05-08 21:07:22 +12:00
def language_ids ( self ) :
return [ language . id for language in self . languages . all ( ) ]
2023-11-30 07:12:17 +13:00
@cache.memoize ( timeout = 500 )
2023-12-08 17:13:38 +13:00
def icon_image ( self , size = ' default ' ) - > str :
2023-08-29 22:01:06 +12:00
if self . icon_id is not None :
2023-12-08 17:13:38 +13:00
if size == ' default ' :
if self . icon . file_path is not None :
if self . icon . file_path . startswith ( ' app/ ' ) :
return self . icon . file_path . replace ( ' app/ ' , ' / ' )
else :
return self . icon . file_path
if self . icon . source_url is not None :
if self . icon . source_url . startswith ( ' app/ ' ) :
return self . icon . source_url . replace ( ' app/ ' , ' / ' )
else :
return self . icon . source_url
elif size == ' tiny ' :
if self . icon . thumbnail_path is not None :
if self . icon . thumbnail_path . startswith ( ' app/ ' ) :
return self . icon . thumbnail_path . replace ( ' app/ ' , ' / ' )
else :
return self . icon . thumbnail_path
if self . icon . source_url is not None :
if self . icon . source_url . startswith ( ' app/ ' ) :
return self . icon . source_url . replace ( ' app/ ' , ' / ' )
else :
return self . icon . source_url
2024-01-05 16:41:50 +13:00
return ' /static/images/1px.gif '
2023-08-29 22:01:06 +12:00
2023-11-30 07:12:17 +13:00
@cache.memoize ( timeout = 500 )
2023-08-29 22:01:06 +12:00
def header_image ( self ) - > str :
if self . image_id is not None :
if self . image . file_path is not None :
2023-12-08 17:13:38 +13:00
if self . image . file_path . startswith ( ' app/ ' ) :
return self . image . file_path . replace ( ' app/ ' , ' / ' )
else :
return self . image . file_path
2023-08-29 22:01:06 +12:00
if self . image . source_url is not None :
2023-12-08 17:13:38 +13:00
if self . image . source_url . startswith ( ' app/ ' ) :
return self . image . source_url . replace ( ' app/ ' , ' / ' )
else :
return self . image . source_url
2023-08-29 22:01:06 +12:00
return ' '
def display_name ( self ) - > str :
if self . ap_id is None :
return self . title
else :
return f " { self . title } @ { self . ap_domain } "
def link ( self ) - > str :
if self . ap_id is None :
return self . name
else :
2024-03-04 21:39:56 +13:00
return self . ap_id . lower ( )
2023-08-10 21:13:37 +12:00
2024-03-13 16:40:20 +13:00
@cache.memoize ( timeout = 3 )
2023-09-17 21:19:51 +12:00
def moderators ( self ) :
return CommunityMember . query . filter ( ( CommunityMember . community_id == self . id ) &
( or_ (
CommunityMember . is_owner ,
CommunityMember . is_moderator
2024-04-06 16:29:47 +13:00
) )
) . filter ( CommunityMember . is_banned == False ) . all ( )
2023-09-17 21:19:51 +12:00
2024-07-17 22:11:31 +08:00
def is_member ( self , user ) :
if user is None :
return CommunityMember . query . filter ( CommunityMember . user_id == current_user . get_id ( ) ,
CommunityMember . community_id == self . id ,
CommunityMember . is_banned == False ) . all ( )
else :
return CommunityMember . query . filter ( CommunityMember . user_id == user . id ,
CommunityMember . community_id == self . id ,
CommunityMember . is_banned == False ) . all ( )
2023-12-26 12:36:02 +13:00
def is_moderator ( self , user = None ) :
if user is None :
2024-07-17 22:11:31 +08:00
return any ( moderator . user_id == current_user . get_id ( ) for moderator in self . moderators ( ) )
2023-12-26 12:36:02 +13:00
else :
return any ( moderator . user_id == user . id for moderator in self . moderators ( ) )
2023-11-30 06:36:08 +13:00
2023-12-26 12:36:02 +13:00
def is_owner ( self , user = None ) :
if user is None :
2024-07-17 22:11:31 +08:00
return any ( moderator . user_id == current_user . get_id ( ) and moderator . is_owner for moderator in self . moderators ( ) )
2023-12-26 12:36:02 +13:00
else :
return any ( moderator . user_id == user . id and moderator . is_owner for moderator in self . moderators ( ) )
2023-12-21 22:14:43 +13:00
2024-02-14 12:31:44 +13:00
def is_instance_admin ( self , user ) :
if self . instance_id :
instance_role = InstanceRole . query . filter ( InstanceRole . instance_id == self . instance_id ,
InstanceRole . user_id == user . id ,
InstanceRole . role == ' admin ' ) . first ( )
return instance_role is not None
else :
return False
2024-01-07 12:47:06 +13:00
def user_is_banned ( self , user ) :
2024-04-29 16:03:00 +12:00
# use communities_banned_from() instead of this method, where possible. Redis caches the result of communities_banned_from()
2024-04-29 16:08:35 +12:00
# we cannot use communities_banned_from() in models.py because it causes a circular import
2024-04-29 16:03:00 +12:00
community_bans = CommunityBan . query . filter ( CommunityBan . user_id == user . id ) . all ( )
return self . id in [ cb . community_id for cb in community_bans ]
2024-01-07 12:47:06 +13:00
2023-12-08 17:13:38 +13:00
def profile_id ( self ) :
2024-03-04 21:39:56 +13:00
retval = self . ap_profile_id if self . ap_profile_id else f " https:// { current_app . config [ ' SERVER_NAME ' ] } /c/ { self . name } "
return retval . lower ( )
2023-12-08 17:13:38 +13:00
2024-03-24 02:12:34 +00:00
def public_url ( self ) :
result = self . ap_public_url if self . ap_public_url else f " https:// { current_app . config [ ' SERVER_NAME ' ] } /c/ { self . name } "
return result
2023-12-09 22:14:16 +13:00
def is_local ( self ) :
return self . ap_id is None or self . profile_id ( ) . startswith ( ' https:// ' + current_app . config [ ' SERVER_NAME ' ] )
2023-12-13 21:04:11 +13:00
def local_url ( self ) :
if self . is_local ( ) :
return self . ap_profile_id
else :
return f " https:// { current_app . config [ ' SERVER_NAME ' ] } /c/ { self . ap_id } "
2024-01-07 12:47:06 +13:00
def notify_new_posts ( self , user_id : int ) - > bool :
2024-04-22 20:53:03 +12:00
existing_notification = NotificationSubscription . query . filter ( NotificationSubscription . entity_id == self . id ,
NotificationSubscription . user_id == user_id ,
NotificationSubscription . type == NOTIF_COMMUNITY ) . first ( )
return existing_notification is not None
# ids of all the users who want to be notified when there is a post in this community
def notification_subscribers ( self ) :
return list ( db . session . execute ( text ( ' SELECT user_id FROM " notification_subscription " WHERE entity_id = :community_id AND type = :type ' ) ,
{ ' community_id ' : self . id , ' type ' : NOTIF_COMMUNITY } ) . scalars ( ) )
2024-01-07 12:47:06 +13:00
2024-01-03 16:29:58 +13:00
# instances that have users which are members of this community. (excluding the current instance)
def following_instances ( self , include_dormant = False ) - > List [ Instance ] :
instances = Instance . query . join ( User , User . instance_id == Instance . id ) . join ( CommunityMember , CommunityMember . user_id == User . id )
instances = instances . filter ( CommunityMember . community_id == self . id , CommunityMember . is_banned == False )
if not include_dormant :
instances = instances . filter ( Instance . dormant == False )
instances = instances . filter ( Instance . id != 1 , Instance . gone_forever == False )
return instances . all ( )
2023-12-26 21:39:52 +13:00
2024-03-24 16:38:20 +00:00
def has_followers_from_domain ( self , domain : str ) - > bool :
instances = Instance . query . join ( User , User . instance_id == Instance . id ) . join ( CommunityMember , CommunityMember . user_id == User . id )
instances = instances . filter ( CommunityMember . community_id == self . id , CommunityMember . is_banned == False )
for instance in instances :
if instance . domain == domain :
return True
return False
2024-05-12 10:31:04 +01:00
def loop_videos ( self ) - > bool :
return ' gifs ' in self . name
2023-12-21 22:14:43 +13:00
def delete_dependencies ( self ) :
for post in self . posts :
post . delete_dependencies ( )
db . session . delete ( post )
db . session . query ( CommunityBan ) . filter ( CommunityBan . community_id == self . id ) . delete ( )
db . session . query ( CommunityBlock ) . filter ( CommunityBlock . community_id == self . id ) . delete ( )
db . session . query ( CommunityJoinRequest ) . filter ( CommunityJoinRequest . community_id == self . id ) . delete ( )
db . session . query ( CommunityMember ) . filter ( CommunityMember . community_id == self . id ) . delete ( )
db . session . query ( Report ) . filter ( Report . suspect_community_id == self . id ) . delete ( )
2024-04-17 15:10:04 +01:00
2023-10-18 22:23:59 +13:00
user_role = db . Table ( ' user_role ' ,
db . Column ( ' user_id ' , db . Integer , db . ForeignKey ( ' user.id ' ) ) ,
db . Column ( ' role_id ' , db . Integer , db . ForeignKey ( ' role.id ' ) ) ,
db . PrimaryKeyConstraint ( ' user_id ' , ' role_id ' )
)
2024-09-27 10:18:49 -04:00
# table to hold users' 'read' post ids
read_posts = db . Table ( ' read_posts ' ,
db . Column ( ' user_id ' , db . Integer , db . ForeignKey ( ' user.id ' ) , index = True ) ,
db . Column ( ' read_post_id ' , db . Integer , db . ForeignKey ( ' post.id ' ) , index = True ) ,
2024-09-30 13:49:06 +13:00
db . Column ( ' interacted_at ' , db . DateTime , index = True , default = utcnow ) # this is when the content is interacted with
2024-09-27 10:18:49 -04:00
)
2023-10-18 22:23:59 +13:00
2024-09-30 13:49:06 +13:00
2023-08-05 21:26:24 +12:00
class User ( UserMixin , db . Model ) :
2023-10-03 22:29:13 +13:00
query_class = FullTextSearchQuery
2023-08-05 21:26:24 +12:00
id = db . Column ( db . Integer , primary_key = True )
2023-11-22 20:48:27 +13:00
user_name = db . Column ( db . String ( 255 ) , index = True )
2024-08-20 07:03:08 +12:00
alt_user_name = db . Column ( db . String ( 255 ) , index = True )
2024-01-01 14:49:15 +13:00
title = db . Column ( db . String ( 256 ) )
2023-08-05 21:26:24 +12:00
email = db . Column ( db . String ( 255 ) , index = True )
password_hash = db . Column ( db . String ( 128 ) )
verified = db . Column ( db . Boolean , default = False )
2023-08-26 15:41:11 +12:00
verification_token = db . Column ( db . String ( 16 ) , index = True )
2023-08-05 21:26:24 +12:00
banned = db . Column ( db . Boolean , default = False )
deleted = db . Column ( db . Boolean , default = False )
2024-10-14 15:48:55 +13:00
deleted_by = db . Column ( db . Integer , index = True )
2023-12-21 22:14:43 +13:00
about = db . Column ( db . Text ) # markdown
about_html = db . Column ( db . Text ) # html
2023-08-05 21:26:24 +12:00
keywords = db . Column ( db . String ( 256 ) )
2023-12-28 21:00:26 +13:00
matrix_user_id = db . Column ( db . String ( 256 ) )
2024-06-28 15:04:06 +00:00
hide_nsfw = db . Column ( db . Integer , default = 1 )
hide_nsfl = db . Column ( db . Integer , default = 1 )
2023-12-12 08:53:35 +13:00
created = db . Column ( db . DateTime , default = utcnow )
last_seen = db . Column ( db . DateTime , default = utcnow , index = True )
2024-02-13 17:22:03 +13:00
avatar_id = db . Column ( db . Integer , db . ForeignKey ( ' file.id ' ) , index = True )
cover_id = db . Column ( db . Integer , db . ForeignKey ( ' file.id ' ) , index = True )
2023-08-05 21:26:24 +12:00
public_key = db . Column ( db . Text )
private_key = db . Column ( db . Text )
newsletter = db . Column ( db . Boolean , default = True )
2024-02-23 16:52:17 +13:00
email_unread = db . Column ( db . Boolean , default = True ) # True if they want to receive 'unread notifications' emails
email_unread_sent = db . Column ( db . Boolean ) # True after a 'unread notifications' email has been sent. None for remote users
receive_message_mode = db . Column ( db . String ( 20 ) , default = ' Closed ' ) # possible values: Open, TrustedOnly, Closed
2023-08-05 21:26:24 +12:00
bounces = db . Column ( db . SmallInteger , default = 0 )
timezone = db . Column ( db . String ( 20 ) )
2023-09-10 20:20:53 +12:00
reputation = db . Column ( db . Float , default = 0.0 )
2023-12-13 21:04:11 +13:00
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
2024-09-13 11:08:04 +12:00
post_count = db . Column ( db . Integer , default = 0 )
post_reply_count = db . Column ( db . Integer , default = 0 )
2023-08-05 21:26:24 +12:00
stripe_customer_id = db . Column ( db . String ( 50 ) )
stripe_subscription_id = db . Column ( db . String ( 50 ) )
searchable = db . Column ( db . Boolean , default = True )
2023-08-26 13:10:01 +12:00
indexable = db . Column ( db . Boolean , default = False )
2023-10-07 21:32:19 +13:00
bot = db . Column ( db . Boolean , default = False )
2024-06-28 13:22:15 +02:00
ignore_bots = db . Column ( db . Integer , default = 0 )
2023-11-30 23:21:37 +13:00
unread_notifications = db . Column ( db . Integer , default = 0 )
2023-12-30 19:03:44 +13:00
ip_address = db . Column ( db . String ( 50 ) )
2024-08-01 16:24:36 +08:00
ip_address_country = db . Column ( db . String ( 50 ) )
2023-12-21 22:14:43 +13:00
instance_id = db . Column ( db . Integer , db . ForeignKey ( ' instance.id ' ) , index = True )
2024-01-01 16:26:57 +13:00
reports = db . Column ( db . Integer , default = 0 ) # how many times this user has been reported.
2024-01-15 18:26:22 +13:00
default_sort = db . Column ( db . String ( 25 ) , default = ' hot ' )
2024-08-16 13:42:29 +12:00
default_filter = db . Column ( db . String ( 25 ) , default = ' subscribed ' )
2024-02-07 17:31:12 +13:00
theme = db . Column ( db . String ( 20 ) , default = ' ' )
2024-02-23 16:52:17 +13:00
referrer = db . Column ( db . String ( 256 ) )
2024-02-26 21:26:19 +13:00
markdown_editor = db . Column ( db . Boolean , default = False )
2024-05-09 13:59:52 +12:00
interface_language = db . Column ( db . String ( 10 ) ) # a locale that the translation system understands e.g. 'en' or 'en-us'. If empty, use browser default
language_id = db . Column ( db . Integer , db . ForeignKey ( ' language.id ' ) ) # the default choice in the language dropdown when composing posts & comments
2024-06-28 18:34:54 +08:00
reply_collapse_threshold = db . Column ( db . Integer , default = - 10 )
reply_hide_threshold = db . Column ( db . Integer , default = - 20 )
2023-08-05 21:26:24 +12:00
2023-12-11 20:46:38 +13:00
avatar = db . relationship ( ' File ' , lazy = ' joined ' , foreign_keys = [ avatar_id ] , single_parent = True , cascade = " all, delete-orphan " )
cover = db . relationship ( ' File ' , lazy = ' joined ' , foreign_keys = [ cover_id ] , single_parent = True , cascade = " all, delete-orphan " )
2023-12-30 11:36:24 +13:00
instance = db . relationship ( ' Instance ' , lazy = ' joined ' , foreign_keys = [ instance_id ] )
2024-02-19 15:01:53 +13:00
conversations = db . relationship ( ' Conversation ' , lazy = ' dynamic ' , secondary = conversation_member , backref = db . backref ( ' members ' , lazy = ' joined ' ) )
2023-09-08 20:04:01 +12:00
ap_id = db . Column ( db . String ( 255 ) , index = True ) # e.g. username@server
2024-11-14 20:16:09 +13:00
ap_profile_id = db . Column ( db . String ( 255 ) , index = True , unique = True ) # e.g. https://server/u/username
2024-06-04 10:01:06 +12:00
ap_public_url = db . Column ( db . String ( 255 ) ) # e.g. https://server/u/UserName
2023-08-05 21:26:24 +12:00
ap_fetched_at = db . Column ( db . DateTime )
ap_followers_url = db . Column ( db . String ( 255 ) )
ap_preferred_username = db . Column ( db . String ( 255 ) )
2024-03-21 23:26:03 +00:00
ap_manually_approves_followers = db . Column ( db . Boolean , default = False )
2023-08-05 21:26:24 +12:00
ap_deleted_at = db . Column ( db . DateTime )
ap_inbox_url = db . Column ( db . String ( 255 ) )
ap_domain = db . Column ( db . String ( 255 ) )
2024-03-01 20:32:29 +13:00
search_vector = db . Column ( TSVectorType ( ' user_name ' , ' about ' , ' keywords ' ) )
2023-08-05 21:26:24 +12:00
activity = db . relationship ( ' ActivityLog ' , backref = ' account ' , lazy = ' dynamic ' , cascade = " all, delete-orphan " )
2023-11-30 07:12:17 +13:00
posts = db . relationship ( ' Post ' , lazy = ' dynamic ' , cascade = " all, delete-orphan " )
2023-12-27 15:47:17 +13:00
post_replies = db . relationship ( ' PostReply ' , lazy = ' dynamic ' , cascade = " all, delete-orphan " )
2023-08-05 21:26:24 +12:00
2023-10-18 22:23:59 +13:00
roles = db . relationship ( ' Role ' , secondary = user_role , lazy = ' dynamic ' , cascade = " all, delete " )
2024-09-27 11:00:20 -04:00
hide_read_posts = db . Column ( db . Boolean , default = False )
2024-09-27 12:36:51 -04:00
# db relationship tracked by the "read_posts" table
# this is the User side, so its referencing the Post side
# read_by is the corresponding Post object variable
read_post = db . relationship ( ' Post ' , secondary = read_posts , back_populates = ' read_by ' , lazy = ' dynamic ' )
2024-09-27 10:18:49 -04:00
2023-08-05 21:26:24 +12:00
def __repr__ ( self ) :
2024-01-18 14:56:23 +13:00
return ' <User {} _ {} > ' . format ( self . user_name , self . id )
2023-08-05 21:26:24 +12:00
def set_password ( self , password ) :
self . password_hash = generate_password_hash ( password )
def check_password ( self , password ) :
try :
result = check_password_hash ( self . password_hash , password )
return result
except Exception :
return False
2024-01-12 12:34:08 +13:00
def get_id ( self ) :
if self . is_authenticated :
return self . id
else :
2024-01-12 13:49:40 +13:00
return 0
2024-01-12 12:34:08 +13:00
2023-10-21 15:49:01 +13:00
def display_name ( self ) :
if self . deleted is False :
2024-01-01 14:49:15 +13:00
if self . title :
2024-10-23 21:35:44 +13:00
return self . title . strip ( )
2024-01-01 14:49:15 +13:00
else :
2024-10-23 21:35:44 +13:00
return self . user_name . strip ( )
2023-10-21 15:49:01 +13:00
else :
return ' [deleted] '
2024-05-26 18:24:13 +12:00
@cache.memoize ( timeout = 500 )
2023-12-24 16:20:18 +13:00
def avatar_thumbnail ( self ) - > str :
if self . avatar_id is not None :
if self . avatar . thumbnail_path is not None :
if self . avatar . thumbnail_path . startswith ( ' app/ ' ) :
return self . avatar . thumbnail_path . replace ( ' app/ ' , ' / ' )
else :
return self . avatar . thumbnail_path
else :
return self . avatar_image ( )
return ' '
2024-05-26 18:24:13 +12:00
@cache.memoize ( timeout = 500 )
2023-10-07 21:32:19 +13:00
def avatar_image ( self ) - > str :
if self . avatar_id is not None :
if self . avatar . file_path is not None :
2023-12-08 17:13:38 +13:00
if self . avatar . file_path . startswith ( ' app/ ' ) :
return self . avatar . file_path . replace ( ' app/ ' , ' / ' )
else :
return self . avatar . file_path
2023-10-07 21:32:19 +13:00
if self . avatar . source_url is not None :
2023-12-08 17:13:38 +13:00
if self . avatar . source_url . startswith ( ' app/ ' ) :
return self . avatar . source_url . replace ( ' app/ ' , ' / ' )
else :
return self . avatar . source_url
2023-10-07 21:32:19 +13:00
return ' '
2024-05-26 18:24:13 +12:00
@cache.memoize ( timeout = 500 )
2023-10-07 21:32:19 +13:00
def cover_image ( self ) - > str :
if self . cover_id is not None :
2024-02-10 06:41:24 +13:00
if self . cover . thumbnail_path is not None :
if self . cover . thumbnail_path . startswith ( ' app/ ' ) :
return self . cover . thumbnail_path . replace ( ' app/ ' , ' / ' )
2023-12-08 17:13:38 +13:00
else :
2024-02-10 06:41:24 +13:00
return self . cover . thumbnail_path
2023-10-07 21:32:19 +13:00
if self . cover . source_url is not None :
2023-12-08 17:13:38 +13:00
if self . cover . source_url . startswith ( ' app/ ' ) :
return self . cover . source_url . replace ( ' app/ ' , ' / ' )
else :
return self . cover . source_url
2023-10-07 21:32:19 +13:00
return ' '
2024-02-10 11:42:18 +13:00
def filesize ( self ) :
size = 0
if self . avatar_id :
size + = self . avatar . filesize ( )
if self . cover_id :
size + = self . cover . filesize ( )
return size
2024-08-20 07:03:08 +12:00
def vote_privately ( self ) :
return self . alt_user_name is not None and self . alt_user_name != ' '
2024-02-10 11:42:18 +13:00
def num_content ( self ) :
content = 0
2024-03-21 11:07:11 +13:00
content + = db . session . execute ( text ( ' SELECT COUNT(id) as c FROM " post " WHERE user_id = :user_id ' ) , { ' user_id ' : self . id } ) . scalar ( )
content + = db . session . execute ( text ( ' SELECT COUNT(id) as c FROM " post_reply " WHERE user_id = :user_id ' ) , { ' user_id ' : self . id } ) . scalar ( )
2024-02-10 11:42:18 +13:00
return content
2023-12-09 22:14:16 +13:00
def is_local ( self ) :
return self . ap_id is None or self . ap_profile_id . startswith ( ' https:// ' + current_app . config [ ' SERVER_NAME ' ] )
2024-02-02 15:30:03 +13:00
def waiting_for_approval ( self ) :
application = UserRegistration . query . filter_by ( user_id = self . id , status = 0 ) . first ( )
return application is not None
2024-01-03 16:29:58 +13:00
@cache.memoize ( timeout = 30 )
2023-12-21 22:14:43 +13:00
def is_admin ( self ) :
for role in self . roles :
if role . name == ' Admin ' :
return True
return False
2024-07-07 15:01:52 +08:00
@cache.memoize ( timeout = 30 )
def is_staff ( self ) :
for role in self . roles :
if role . name == ' Staff ' :
return True
return False
def is_instance_admin ( self ) :
if self . instance_id :
instance_role = InstanceRole . query . filter ( InstanceRole . instance_id == self . instance_id ,
InstanceRole . user_id == self . id ,
InstanceRole . role == ' admin ' ) . first ( )
return instance_role is not None
else :
return False
2024-03-11 20:14:12 +13:00
def trustworthy ( self ) :
if self . is_admin ( ) :
return True
if self . created_recently ( ) or self . reputation < 100 :
return False
return True
2024-09-13 11:08:04 +12:00
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
2023-10-10 22:25:37 +13:00
def link ( self ) - > str :
2023-12-09 22:14:16 +13:00
if self . is_local ( ) :
2023-10-10 22:25:37 +13:00
return self . user_name
else :
2024-03-04 21:46:23 +13:00
return self . ap_id
2023-10-10 22:25:37 +13:00
2023-12-09 22:14:16 +13:00
def followers_url ( self ) :
if self . ap_followers_url :
return self . ap_followers_url
else :
2024-06-05 13:21:41 +12:00
return self . public_url ( ) + ' /followers '
2023-12-09 22:14:16 +13:00
2024-10-13 10:53:47 +13:00
def instance_domain ( self ) :
if self . ap_domain :
return self . ap_domain
if self . is_local ( ) :
return current_app . config [ ' SERVER_NAME ' ]
else :
return self . instance . domain
2023-08-05 21:26:24 +12:00
def get_reset_password_token ( self , expires_in = 600 ) :
return jwt . encode (
{ ' reset_password ' : self . id , ' exp ' : time ( ) + expires_in } ,
current_app . config [ ' SECRET_KEY ' ] ,
2023-09-03 16:30:20 +12:00
algorithm = ' HS256 ' )
2023-08-05 21:26:24 +12:00
def another_account_using_email ( self , email ) :
another_account = User . query . filter ( User . email == email , User . id != self . id ) . first ( )
return another_account is not None
def expires_soon ( self ) :
if self . expires is None :
return False
2023-12-12 08:53:35 +13:00
return self . expires < utcnow ( ) + timedelta ( weeks = 1 )
2023-08-05 21:26:24 +12:00
def is_expired ( self ) :
if self . expires is None :
return True
2023-12-12 08:53:35 +13:00
return self . expires < utcnow ( )
2023-08-05 21:26:24 +12:00
def expired_ages_ago ( self ) :
if self . expires is None :
return True
return self . expires < datetime ( 2019 , 9 , 1 )
2023-12-27 19:51:07 +13:00
def recalculate_attitude ( self ) :
2024-09-13 11:08:04 +12:00
upvotes = downvotes = 0
2024-11-02 16:34:16 +13:00
with db . session . no_autoflush : # Avoid StaleDataError exception
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 = 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
2023-12-27 19:51:07 +13:00
total_upvotes = upvotes + comment_upvotes
total_downvotes = downvotes + comment_downvotes
if total_downvotes == 0 : # guard against division by zero
self . attitude = 1.0
else :
2024-06-12 07:47:04 +08:00
if total_upvotes + total_downvotes > 2 : # Only calculate attitude if they've done 3 or more votes as anything less than this could be an outlier and not representative of their overall attitude
self . attitude = ( total_upvotes - total_downvotes ) / ( total_upvotes + total_downvotes )
else :
self . attitude = 1.0
2023-12-27 19:51:07 +13:00
2024-09-13 11:08:04 +12:00
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 ( )
2023-12-03 22:41:15 +13:00
def subscribed ( self , community_id : int ) - > int :
if community_id is None :
2023-08-29 22:01:06 +12:00
return False
2023-12-03 22:41:15 +13:00
subscription : CommunityMember = CommunityMember . query . filter_by ( user_id = self . id , community_id = community_id ) . first ( )
2023-08-29 22:01:06 +12:00
if subscription :
2023-09-08 20:04:01 +12:00
if subscription . is_banned :
return SUBSCRIPTION_BANNED
elif subscription . is_owner :
2023-08-29 22:01:06 +12:00
return SUBSCRIPTION_OWNER
elif subscription . is_moderator :
return SUBSCRIPTION_MODERATOR
else :
return SUBSCRIPTION_MEMBER
else :
2023-12-03 22:41:15 +13:00
join_request = CommunityJoinRequest . query . filter_by ( user_id = self . id , community_id = community_id ) . first ( )
if join_request :
return SUBSCRIPTION_PENDING
else :
return SUBSCRIPTION_NONMEMBER
2023-08-29 22:01:06 +12:00
2023-09-17 21:19:51 +12:00
def communities ( self ) - > List [ Community ] :
return Community . query . filter ( Community . banned == False ) . \
2024-01-28 18:11:32 +13:00
join ( CommunityMember ) . filter ( CommunityMember . is_banned == False , CommunityMember . user_id == self . id ) . all ( )
2023-09-17 21:19:51 +12:00
2023-11-17 22:02:44 +13:00
def profile_id ( self ) :
2024-06-04 09:44:10 +12:00
result = self . ap_profile_id if self . ap_profile_id else f " https:// { current_app . config [ ' SERVER_NAME ' ] } /u/ { self . user_name . lower ( ) } "
2024-03-04 21:46:23 +13:00
return result
2023-11-17 22:02:44 +13:00
2024-08-20 07:03:08 +12:00
def public_url ( self , main_user_name = True ) :
if main_user_name :
result = self . ap_public_url if self . ap_public_url else f " https:// { current_app . config [ ' SERVER_NAME ' ] } /u/ { self . user_name } "
else :
result = f " https:// { current_app . config [ ' SERVER_NAME ' ] } /u/ { self . alt_user_name } "
2024-03-24 02:12:34 +00:00
return result
2023-11-30 05:14:22 +13:00
def created_recently ( self ) :
2024-06-26 11:22:31 +02:00
if self . is_admin ( ) :
return False
2023-12-12 08:53:35 +13:00
return self . created and self . created > utcnow ( ) - timedelta ( days = 7 )
2023-11-30 05:14:22 +13:00
2024-01-01 16:26:57 +13:00
def has_blocked_instance ( self , instance_id : int ) :
2023-12-26 21:39:52 +13:00
instance_block = InstanceBlock . query . filter_by ( user_id = self . id , instance_id = instance_id ) . first ( )
return instance_block is not None
2024-01-01 16:26:57 +13:00
def has_blocked_user ( self , user_id : int ) :
existing_block = UserBlock . query . filter_by ( blocker_id = self . id , blocked_id = user_id ) . first ( )
return existing_block is not None
2023-08-05 21:26:24 +12:00
@staticmethod
def verify_reset_password_token ( token ) :
try :
id = jwt . decode ( token , current_app . config [ ' SECRET_KEY ' ] ,
algorithms = [ ' HS256 ' ] ) [ ' reset_password ' ]
except :
return
return User . query . get ( id )
2024-01-01 14:49:15 +13:00
def delete_dependencies ( self ) :
if self . cover_id :
file = File . query . get ( self . cover_id )
file . delete_from_disk ( )
self . cover_id = None
db . session . delete ( file )
if self . avatar_id :
file = File . query . get ( self . avatar_id )
file . delete_from_disk ( )
self . avatar_id = None
db . session . delete ( file )
2024-02-02 15:30:03 +13:00
if self . waiting_for_approval ( ) :
db . session . query ( UserRegistration ) . filter ( UserRegistration . user_id == self . id ) . delete ( )
2024-04-22 20:53:03 +12:00
db . session . query ( NotificationSubscription ) . filter ( NotificationSubscription . user_id == self . id ) . delete ( )
db . session . query ( Notification ) . filter ( Notification . user_id == self . id ) . delete ( )
2024-05-18 19:41:20 +12:00
db . session . query ( PollChoiceVote ) . filter ( PollChoiceVote . user_id == self . id ) . delete ( )
2024-06-22 07:49:14 +08:00
db . session . query ( PostBookmark ) . filter ( PostBookmark . user_id == self . id ) . delete ( )
db . session . query ( PostReplyBookmark ) . filter ( PostReplyBookmark . user_id == self . id ) . delete ( )
2024-01-01 14:49:15 +13:00
2024-06-02 16:45:21 +12:00
def purge_content ( self , soft = True ) :
2023-11-30 23:21:37 +13:00
files = File . query . join ( Post ) . filter ( Post . user_id == self . id ) . all ( )
for file in files :
file . delete_from_disk ( )
2024-01-01 14:49:15 +13:00
self . delete_dependencies ( )
2024-01-09 20:44:08 +13:00
posts = Post . query . filter_by ( user_id = self . id ) . all ( )
for post in posts :
post . delete_dependencies ( )
2024-06-02 16:45:21 +12:00
if soft :
post . deleted = True
else :
db . session . delete ( post )
2024-04-14 21:49:42 +12:00
db . session . commit ( )
2024-02-09 12:52:16 +13:00
post_replies = PostReply . query . filter_by ( user_id = self . id ) . all ( )
for reply in post_replies :
2024-04-14 21:49:42 +12:00
reply . delete_dependencies ( )
2024-06-02 16:45:21 +12:00
if soft :
reply . deleted = True
else :
db . session . delete ( reply )
2024-01-09 20:44:08 +13:00
db . session . commit ( )
2023-10-21 15:49:01 +13:00
2024-03-24 02:19:49 +00:00
def mention_tag ( self ) :
if self . ap_domain is None :
return ' @ ' + self . user_name + ' @ ' + current_app . config [ ' SERVER_NAME ' ]
else :
return ' @ ' + self . user_name + ' @ ' + self . ap_domain
2024-04-19 20:06:08 +12:00
# True if user_id wants to be notified about posts by self
def notify_new_posts ( self , user_id ) :
existing_notification = NotificationSubscription . query . filter ( NotificationSubscription . entity_id == self . id ,
NotificationSubscription . user_id == user_id ,
NotificationSubscription . type == NOTIF_USER ) . first ( )
return existing_notification is not None
# ids of all the users who want to be notified when self makes a post
def notification_subscribers ( self ) :
2024-04-19 20:13:02 +12:00
return list ( db . session . execute ( text ( ' SELECT user_id FROM " notification_subscription " WHERE entity_id = :user_id AND type = :type ' ) ,
{ ' user_id ' : self . id , ' type ' : NOTIF_USER } ) . scalars ( ) )
2024-04-19 20:06:08 +12:00
2024-09-20 16:06:08 +00:00
def encode_jwt_token ( self ) :
2024-10-27 13:36:17 +00:00
payload = { ' sub ' : str ( self . id ) , ' iss ' : current_app . config [ ' SERVER_NAME ' ] , ' iat ' : int ( time ( ) ) }
return jwt . encode ( payload , current_app . config [ ' SECRET_KEY ' ] , algorithm = ' HS256 ' )
2024-09-20 16:06:08 +00:00
2024-09-27 10:18:49 -04:00
# mark a post as 'read' for this user
2024-09-27 12:36:51 -04:00
def mark_post_as_read ( self , post ) :
2024-09-27 10:18:49 -04:00
# check if its already marked as read, if not, mark it as read
2024-09-27 12:36:51 -04:00
if not self . has_read_post ( post ) :
self . read_post . append ( post )
2024-09-27 10:18:49 -04:00
# check if post has been read by this user
# returns true if the post has been read, false if not
2024-09-27 12:36:51 -04:00
def has_read_post ( self , post ) :
return self . read_post . filter ( read_posts . c . read_post_id == post . id ) . count ( ) > 0
2024-09-27 10:18:49 -04:00
2023-10-21 15:49:01 +13:00
2023-08-05 21:26:24 +12:00
class ActivityLog ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
activity_type = db . Column ( db . String ( 64 ) )
activity = db . Column ( db . String ( 255 ) )
2023-12-12 08:53:35 +13:00
timestamp = db . Column ( db . DateTime , index = True , default = utcnow )
2023-08-05 21:26:24 +12:00
class Post ( db . Model ) :
2023-10-03 22:29:13 +13:00
query_class = FullTextSearchQuery
2023-08-05 21:26:24 +12:00
id = db . Column ( db . Integer , primary_key = True )
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
community_id = db . Column ( db . Integer , db . ForeignKey ( ' community.id ' ) , index = True )
image_id = db . Column ( db . Integer , db . ForeignKey ( ' file.id ' ) , index = True )
domain_id = db . Column ( db . Integer , db . ForeignKey ( ' domain.id ' ) , index = True )
2023-12-13 21:04:11 +13:00
instance_id = db . Column ( db . Integer , db . ForeignKey ( ' instance.id ' ) , index = True )
2024-11-02 15:14:31 +13:00
licence_id = db . Column ( db . Integer , db . ForeignKey ( ' licence.id ' ) , index = True )
2023-08-05 21:26:24 +12:00
slug = db . Column ( db . String ( 255 ) )
title = db . Column ( db . String ( 255 ) )
url = db . Column ( db . String ( 2048 ) )
body = db . Column ( db . Text )
2023-08-10 21:13:37 +12:00
body_html = db . Column ( db . Text )
2024-10-14 15:37:00 +13:00
type = db . Column ( db . Integer , default = constants . POST_TYPE_ARTICLE )
2024-08-08 19:04:50 +12:00
microblog = db . Column ( db . Boolean , default = False )
2023-09-16 19:09:04 +12:00
comments_enabled = db . Column ( db . Boolean , default = True )
2024-06-02 16:45:21 +12:00
deleted = db . Column ( db . Boolean , default = False , index = True )
2024-10-14 15:48:55 +13:00
deleted_by = db . Column ( db . Integer , index = True )
2023-12-14 21:22:46 +13:00
mea_culpa = db . Column ( db . Boolean , default = False )
2023-08-05 21:26:24 +12:00
has_embed = db . Column ( db . Boolean , default = False )
reply_count = db . Column ( db . Integer , default = 0 )
2023-12-15 17:35:11 +13:00
score = db . Column ( db . Integer , default = 0 , index = True ) # used for 'top' ranking
2024-02-13 17:22:03 +13:00
nsfw = db . Column ( db . Boolean , default = False , index = True )
nsfl = db . Column ( db . Boolean , default = False , index = True )
2023-08-05 21:26:24 +12:00
sticky = db . Column ( db . Boolean , default = False )
2023-11-30 20:57:51 +13:00
notify_author = db . Column ( db . Boolean , default = True )
2024-03-12 20:58:47 +13:00
indexable = db . Column ( db . Boolean , default = True )
2024-02-13 17:22:03 +13:00
from_bot = db . Column ( db . Boolean , default = False , index = True )
2023-12-12 08:53:35 +13:00
created_at = db . Column ( db . DateTime , index = True , default = utcnow ) # this is when the content arrived here
posted_at = db . Column ( db . DateTime , index = True , default = utcnow ) # this is when the original server created it
last_active = db . Column ( db . DateTime , index = True , default = utcnow )
2023-08-05 21:26:24 +12:00
ip = db . Column ( db . String ( 50 ) )
up_votes = db . Column ( db . Integer , default = 0 )
down_votes = db . Column ( db . Integer , default = 0 )
2024-02-13 17:22:03 +13:00
ranking = db . Column ( db . Integer , default = 0 , index = True ) # used for 'hot' ranking
2023-08-05 21:26:24 +12:00
edited_at = db . Column ( db . DateTime )
2023-12-17 00:12:49 +13:00
reports = db . Column ( db . Integer , default = 0 ) # how many times this post has been reported. Set to -1 to ignore reports
2024-05-09 17:54:30 +12:00
language_id = db . Column ( db . Integer , db . ForeignKey ( ' language.id ' ) , index = True )
2024-03-31 02:15:10 +01:00
cross_posts = db . Column ( MutableList . as_mutable ( ARRAY ( db . Integer ) ) )
2024-04-16 21:23:19 +12:00
tags = db . relationship ( ' Tag ' , lazy = ' dynamic ' , secondary = post_tag , backref = db . backref ( ' posts ' , lazy = ' dynamic ' ) )
2023-08-05 21:26:24 +12:00
ap_id = db . Column ( db . String ( 255 ) , index = True )
2023-08-10 21:13:37 +12:00
ap_create_id = db . Column ( db . String ( 100 ) )
ap_announce_id = db . Column ( db . String ( 100 ) )
2023-08-05 21:26:24 +12:00
search_vector = db . Column ( TSVectorType ( ' title ' , ' body ' ) )
2023-11-30 07:12:17 +13:00
image = db . relationship ( File , lazy = ' joined ' , foreign_keys = [ image_id ] , cascade = " all, delete " )
domain = db . relationship ( ' Domain ' , lazy = ' joined ' , foreign_keys = [ domain_id ] )
2023-12-03 22:41:15 +13:00
author = db . relationship ( ' User ' , lazy = ' joined ' , overlaps = ' posts ' , foreign_keys = [ user_id ] )
2024-01-02 19:41:00 +13:00
community = db . relationship ( ' Community ' , lazy = ' joined ' , overlaps = ' posts ' , foreign_keys = [ community_id ] )
2023-12-10 15:10:09 +13:00
replies = db . relationship ( ' PostReply ' , lazy = ' dynamic ' , backref = ' post ' )
2024-05-09 17:54:30 +12:00
language = db . relationship ( ' Language ' , foreign_keys = [ language_id ] )
2024-11-02 16:02:29 +13:00
licence = db . relationship ( ' Licence ' , foreign_keys = [ licence_id ] )
2023-08-10 21:13:37 +12:00
2024-09-27 12:36:51 -04:00
# db relationship tracked by the "read_posts" table
# this is the Post side, so its referencing the User side
# read_post is the corresponding User object variable
read_by = db . relationship ( ' User ' , secondary = read_posts , back_populates = ' read_post ' , lazy = ' dynamic ' )
2023-12-09 22:14:16 +13:00
def is_local ( self ) :
return self . ap_id is None or self . ap_id . startswith ( ' https:// ' + current_app . config [ ' SERVER_NAME ' ] )
2023-09-16 19:09:04 +12:00
@classmethod
def get_by_ap_id ( cls , ap_id ) :
return cls . query . filter_by ( ap_id = ap_id ) . first ( )
2024-10-14 15:37:00 +13:00
@classmethod
def new ( cls , user : User , community : Community , request_json : dict , announce_id = None ) :
2024-10-20 20:21:30 +13:00
from app . activitypub . util import instance_weight , find_language_or_create , find_language , find_hashtag_or_create , \
2024-11-02 16:02:29 +13:00
find_licence_or_create , make_image_sizes , notify_about_post
2024-10-14 15:37:00 +13:00
from app . utils import allowlist_html , markdown_to_html , html_to_text , microblog_content_to_title , blocked_phrases , \
is_image_url , is_video_url , domain_from_url , opengraph_parse , shorten_string , remove_tracking_from_link , \
2024-10-22 19:51:37 +13:00
is_video_hosting_site , communities_banned_from
2024-10-14 15:37:00 +13:00
microblog = False
if ' name ' not in request_json [ ' object ' ] : # Microblog posts
if ' content ' in request_json [ ' object ' ] and request_json [ ' object ' ] [ ' content ' ] is not None :
title = " [Microblog] "
microblog = True
else :
return None
else :
title = request_json [ ' object ' ] [ ' name ' ] . strip ( )
nsfl_in_title = ' [NSFL] ' in title . upper ( ) or ' (NSFL) ' in title . upper ( )
post = Post ( user_id = user . id , community_id = community . id ,
title = html . unescape ( title ) ,
comments_enabled = request_json [ ' object ' ] [ ' commentsEnabled ' ] if ' commentsEnabled ' in request_json [ ' object ' ] else True ,
sticky = request_json [ ' object ' ] [ ' stickied ' ] if ' stickied ' in request_json [ ' object ' ] else False ,
nsfw = request_json [ ' object ' ] [ ' sensitive ' ] if ' sensitive ' in request_json [ ' object ' ] else False ,
nsfl = request_json [ ' object ' ] [ ' nsfl ' ] if ' nsfl ' in request_json [ ' object ' ] else nsfl_in_title ,
ap_id = request_json [ ' object ' ] [ ' id ' ] ,
ap_create_id = request_json [ ' id ' ] ,
ap_announce_id = announce_id ,
up_votes = 1 ,
from_bot = user . bot ,
score = instance_weight ( user . ap_domain ) ,
instance_id = user . instance_id ,
indexable = user . indexable ,
2024-10-20 20:21:30 +13:00
microblog = microblog ,
posted_at = utcnow ( )
2024-10-14 15:37:00 +13:00
)
if ' content ' in request_json [ ' object ' ] and request_json [ ' object ' ] [ ' content ' ] is not None :
if ' mediaType ' in request_json [ ' object ' ] and request_json [ ' object ' ] [ ' mediaType ' ] == ' text/html ' :
post . body_html = allowlist_html ( request_json [ ' object ' ] [ ' content ' ] )
if ' source ' in request_json [ ' object ' ] and isinstance ( request_json [ ' object ' ] [ ' source ' ] , dict ) and \
request_json [ ' object ' ] [ ' source ' ] [ ' mediaType ' ] == ' text/markdown ' :
post . body = request_json [ ' object ' ] [ ' source ' ] [ ' content ' ]
post . body_html = markdown_to_html ( post . body ) # prefer Markdown if provided, overwrite version obtained from HTML
else :
post . body = html_to_text ( post . body_html )
elif ' mediaType ' in request_json [ ' object ' ] and request_json [ ' object ' ] [ ' mediaType ' ] == ' text/markdown ' :
post . body = request_json [ ' object ' ] [ ' content ' ]
post . body_html = markdown_to_html ( post . body )
else :
if not ( request_json [ ' object ' ] [ ' content ' ] . startswith ( ' <p> ' ) or request_json [ ' object ' ] [ ' content ' ] . startswith ( ' <blockquote> ' ) ) :
request_json [ ' object ' ] [ ' content ' ] = ' <p> ' + request_json [ ' object ' ] [ ' content ' ] + ' </p> '
post . body_html = allowlist_html ( request_json [ ' object ' ] [ ' content ' ] )
post . body = html_to_text ( post . body_html )
if microblog :
autogenerated_title = microblog_content_to_title ( post . body_html )
if len ( autogenerated_title ) < 20 :
title = ' [Microblog] ' + autogenerated_title . strip ( )
else :
title = autogenerated_title . strip ( )
if ' [NSFL] ' in title . upper ( ) or ' (NSFL) ' in title . upper ( ) :
post . nsfl = True
if ' [NSFW] ' in title . upper ( ) or ' (NSFW) ' in title . upper ( ) :
post . nsfw = True
post . title = title
# Discard post if it contains certain phrases. Good for stopping spam floods.
blocked_phrases_list = blocked_phrases ( )
for blocked_phrase in blocked_phrases_list :
if blocked_phrase in post . title :
return None
if post . body :
for blocked_phrase in blocked_phrases_list :
if blocked_phrase in post . body :
return None
if ' attachment ' in request_json [ ' object ' ] and len ( request_json [ ' object ' ] [ ' attachment ' ] ) > 0 and \
' type ' in request_json [ ' object ' ] [ ' attachment ' ] [ 0 ] :
alt_text = None
if request_json [ ' object ' ] [ ' attachment ' ] [ 0 ] [ ' type ' ] == ' Link ' :
post . url = request_json [ ' object ' ] [ ' attachment ' ] [ 0 ] [ ' href ' ] # Lemmy < 0.19.4
if request_json [ ' object ' ] [ ' attachment ' ] [ 0 ] [ ' type ' ] == ' Document ' :
post . url = request_json [ ' object ' ] [ ' attachment ' ] [ 0 ] [ ' url ' ] # Mastodon
if ' name ' in request_json [ ' object ' ] [ ' attachment ' ] [ 0 ] :
alt_text = request_json [ ' object ' ] [ ' attachment ' ] [ 0 ] [ ' name ' ]
if request_json [ ' object ' ] [ ' attachment ' ] [ 0 ] [ ' type ' ] == ' Image ' :
post . url = request_json [ ' object ' ] [ ' attachment ' ] [ 0 ] [ ' url ' ] # PixelFed, PieFed, Lemmy >= 0.19.4
if ' name ' in request_json [ ' object ' ] [ ' attachment ' ] [ 0 ] :
alt_text = request_json [ ' object ' ] [ ' attachment ' ] [ 0 ] [ ' name ' ]
if post . url :
if is_image_url ( post . url ) :
post . type = constants . POST_TYPE_IMAGE
2024-11-17 22:14:39 +00:00
image = File ( source_url = post . url )
2024-10-14 15:37:00 +13:00
if alt_text :
image . alt_text = alt_text
db . session . add ( image )
post . image = image
elif is_video_url ( post . url ) : # youtube is detected later
post . type = constants . POST_TYPE_VIDEO
image = File ( source_url = post . url )
db . session . add ( image )
post . image = image
else :
post . type = constants . POST_TYPE_LINK
domain = domain_from_url ( post . url )
# notify about links to banned websites.
already_notified = set ( ) # often admins and mods are the same people - avoid notifying them twice
if domain . notify_mods :
for community_member in post . community . moderators ( ) :
notify = Notification ( title = ' Suspicious content ' , url = post . ap_id ,
user_id = community_member . user_id ,
author_id = user . id )
db . session . add ( notify )
already_notified . add ( community_member . user_id )
if domain . notify_admins :
for admin in Site . admins ( ) :
if admin . id not in already_notified :
notify = Notification ( title = ' Suspicious content ' ,
url = post . ap_id , user_id = admin . id ,
author_id = user . id )
db . session . add ( notify )
if domain . banned or domain . name . endswith ( ' .pages.dev ' ) :
raise Exception ( domain . name + ' is blocked by admin ' )
else :
domain . post_count + = 1
post . domain = domain
if post is not None :
if request_json [ ' object ' ] [ ' type ' ] == ' Video ' :
post . type = constants . POST_TYPE_VIDEO
post . url = request_json [ ' object ' ] [ ' id ' ]
if ' icon ' in request_json [ ' object ' ] and isinstance ( request_json [ ' object ' ] [ ' icon ' ] , list ) :
icon = File ( source_url = request_json [ ' object ' ] [ ' icon ' ] [ - 1 ] [ ' url ' ] )
db . session . add ( icon )
post . image = icon
# Language. Lemmy uses 'language' while Mastodon has 'contentMap'
if ' language ' in request_json [ ' object ' ] and isinstance ( request_json [ ' object ' ] [ ' language ' ] , dict ) :
language = find_language_or_create ( request_json [ ' object ' ] [ ' language ' ] [ ' identifier ' ] ,
request_json [ ' object ' ] [ ' language ' ] [ ' name ' ] )
2024-11-02 16:02:29 +13:00
post . language = language
2024-10-14 15:37:00 +13:00
elif ' contentMap ' in request_json [ ' object ' ] and isinstance ( request_json [ ' object ' ] [ ' contentMap ' ] , dict ) :
language = find_language ( next ( iter ( request_json [ ' object ' ] [ ' contentMap ' ] ) ) )
post . language_id = language . id if language else None
2024-11-02 16:02:29 +13:00
if ' licence ' in request_json [ ' object ' ] and isinstance ( request_json [ ' object ' ] [ ' licence ' ] , dict ) :
licence = find_licence_or_create ( request_json [ ' object ' ] [ ' licence ' ] [ ' name ' ] )
post . licence = licence
2024-10-14 15:37:00 +13:00
if ' tag ' in request_json [ ' object ' ] and isinstance ( request_json [ ' object ' ] [ ' tag ' ] , list ) :
for json_tag in request_json [ ' object ' ] [ ' tag ' ] :
if json_tag and json_tag [ ' type ' ] == ' Hashtag ' :
if json_tag [ ' name ' ] [ 1 : ] . lower ( ) != community . name . lower ( ) : # Lemmy adds the community slug as a hashtag on every post in the community, which we want to ignore
hashtag = find_hashtag_or_create ( json_tag [ ' name ' ] )
if hashtag :
post . tags . append ( hashtag )
if ' image ' in request_json [ ' object ' ] and post . image is None :
image = File ( source_url = request_json [ ' object ' ] [ ' image ' ] [ ' url ' ] )
db . session . add ( image )
post . image = image
if post . image is None and post . type == constants . POST_TYPE_LINK : # This is a link post but the source instance has not provided a thumbnail image
# Let's see if we can do better than the source instance did!
tn_url = post . url
if tn_url [ : 32 ] == ' https://www.youtube.com/watch?v= ' :
tn_url = ' https://youtu.be/ ' + tn_url [
32 : 43 ] # better chance of thumbnail from youtu.be than youtube.com
opengraph = opengraph_parse ( tn_url )
if opengraph and ( opengraph . get ( ' og:image ' , ' ' ) != ' ' or opengraph . get ( ' og:image:url ' , ' ' ) != ' ' ) :
filename = opengraph . get ( ' og:image ' ) or opengraph . get ( ' og:image:url ' )
if not filename . startswith ( ' / ' ) :
file = File ( source_url = filename , alt_text = shorten_string ( opengraph . get ( ' og:title ' ) , 295 ) )
post . image = file
db . session . add ( file )
if ' searchableBy ' in request_json [ ' object ' ] and request_json [ ' object ' ] [ ' searchableBy ' ] != ' https://www.w3.org/ns/activitystreams#Public ' :
post . indexable = False
if post . url :
post . url = remove_tracking_from_link ( post . url ) # moved here as changes youtu.be to youtube.com
if is_video_hosting_site ( post . url ) :
post . type = constants . POST_TYPE_VIDEO
db . session . add ( post )
2024-10-20 20:21:30 +13:00
post . ranking = post . post_ranking ( post . score , post . posted_at )
2024-10-14 15:37:00 +13:00
community . post_count + = 1
community . last_active = utcnow ( )
user . post_count + = 1
db . session . commit ( )
# Polls need to be processed quite late because they need a post_id to refer to
if request_json [ ' object ' ] [ ' type ' ] == ' Question ' :
post . type = constants . POST_TYPE_POLL
mode = ' single '
if ' anyOf ' in request_json [ ' object ' ] :
mode = ' multiple '
poll = Poll ( post_id = post . id , end_poll = request_json [ ' object ' ] [ ' endTime ' ] , mode = mode , local_only = False )
db . session . add ( poll )
i = 1
for choice_ap in request_json [ ' object ' ] [ ' oneOf ' if mode == ' single ' else ' anyOf ' ] :
new_choice = PollChoice ( post_id = post . id , choice_text = choice_ap [ ' name ' ] , sort_order = i )
db . session . add ( new_choice )
i + = 1
db . session . commit ( )
if post . image_id :
make_image_sizes ( post . image_id , 170 , 512 , ' posts ' ,
community . low_quality ) # the 512 sized image is for masonry view
# Update list of cross posts
if post . url :
other_posts = Post . query . filter ( Post . id != post . id , Post . url == post . url , Post . deleted == False ,
Post . posted_at > post . posted_at - timedelta ( days = 6 ) ) . all ( )
for op in other_posts :
if op . cross_posts is None :
op . cross_posts = [ post . id ]
else :
op . cross_posts . append ( post . id )
if post . cross_posts is None :
post . cross_posts = [ op . id ]
else :
post . cross_posts . append ( op . id )
db . session . commit ( )
if post . community_id not in communities_banned_from ( user . id ) :
notify_about_post ( post )
if user . reputation > 100 :
post . up_votes + = 1
post . score + = 1
2024-10-20 20:21:30 +13:00
post . ranking = post . post_ranking ( post . score , post . posted_at )
2024-10-14 15:37:00 +13:00
db . session . commit ( )
return post
# All the following post/comment ranking math is explained at https://medium.com/hacking-and-gonzo/how-reddit-ranking-algorithms-work-ef111e33d0d9
epoch = datetime ( 1970 , 1 , 1 )
@classmethod
def epoch_seconds ( self , date ) :
td = date - self . epoch
return td . days * 86400 + td . seconds + ( float ( td . microseconds ) / 1000000 )
2023-11-21 23:05:07 +13:00
def delete_dependencies ( self ) :
2024-06-22 07:49:14 +08:00
db . session . query ( PostBookmark ) . filter ( PostBookmark . post_id == self . id ) . delete ( )
2024-05-18 19:41:20 +12:00
db . session . query ( PollChoiceVote ) . filter ( PollChoiceVote . post_id == self . id ) . delete ( )
db . session . query ( PollChoice ) . filter ( PollChoice . post_id == self . id ) . delete ( )
db . session . query ( Poll ) . filter ( Poll . post_id == self . id ) . delete ( )
2023-12-17 00:12:49 +13:00
db . session . query ( Report ) . filter ( Report . suspect_post_id == self . id ) . delete ( )
2024-09-07 14:15:34 +12:00
db . session . execute ( text ( ' DELETE FROM " post_vote " WHERE post_id = :post_id ' ) , { ' post_id ' : self . id } )
2024-10-25 06:17:10 +00:00
reply_ids = db . session . execute ( text ( ' SELECT id FROM " post_reply " WHERE post_id = :post_id ' ) , { ' post_id ' : self . id } ) . scalars ( )
reply_ids = tuple ( reply_ids )
if reply_ids :
db . session . execute ( text ( ' DELETE FROM " post_reply_vote " WHERE post_reply_id IN :reply_ids ' ) , { ' reply_ids ' : reply_ids } )
db . session . execute ( text ( ' DELETE FROM " post_reply_bookmark " WHERE post_reply_id IN :reply_ids ' ) , { ' reply_ids ' : reply_ids } )
db . session . execute ( text ( ' DELETE FROM " report " WHERE suspect_post_reply_id IN :reply_ids ' ) , { ' reply_ids ' : reply_ids } )
db . session . execute ( text ( ' DELETE FROM " post_reply " WHERE post_id = :post_id ' ) , { ' post_id ' : self . id } )
2024-10-26 04:46:51 +00:00
self . community . post_reply_count = db . session . execute ( text ( ' SELECT COUNT(id) as c FROM " post_reply " WHERE community_id = :community_id AND deleted = false ' ) ,
{ ' community_id ' : self . community_id } ) . scalar ( )
2023-11-30 20:57:51 +13:00
if self . image_id :
file = File . query . get ( self . image_id )
file . delete_from_disk ( )
2023-11-21 23:05:07 +13:00
2024-09-07 12:36:21 +12:00
def youtube_embed ( self , rel = True ) - > str :
2023-11-29 20:32:07 +13:00
if self . url :
2024-09-07 11:33:20 +12:00
parsed_url = urlparse ( self . url )
query_params = parse_qs ( parsed_url . query )
if ' v ' in query_params :
video_id = query_params . pop ( ' v ' ) [ 0 ]
2024-09-07 12:36:21 +12:00
if rel :
query_params [ ' rel ' ] = ' 0 '
2024-09-07 11:33:20 +12:00
new_query = urlencode ( query_params , doseq = True )
return f ' { video_id } ? { new_query } '
if ' /shorts/ ' in parsed_url . path :
video_id = parsed_url . path . split ( ' /shorts/ ' ) [ 1 ] . split ( ' / ' ) [ 0 ]
if ' t ' in query_params :
query_params [ ' start ' ] = query_params . pop ( ' t ' ) [ 0 ]
2024-09-07 12:36:21 +12:00
if rel :
query_params [ ' rel ' ] = ' 0 '
2024-09-07 11:33:20 +12:00
new_query = urlencode ( query_params , doseq = True )
return f ' { video_id } ? { new_query } '
return ' '
2023-11-29 20:32:07 +13:00
2024-10-06 07:03:58 +13:00
def youtube_video_id ( self ) - > str :
if self . url :
parsed_url = urlparse ( self . url )
query_params = parse_qs ( parsed_url . query )
if ' v ' in query_params :
return query_params [ ' v ' ] [ 0 ]
if ' /shorts/ ' in parsed_url . path :
video_id = parsed_url . path . split ( ' /shorts/ ' ) [ 1 ] . split ( ' / ' ) [ 0 ]
return f ' { video_id } '
return ' '
2024-05-26 02:19:57 +01:00
def peertube_embed ( self ) :
if self . url :
return self . url . replace ( ' watch ' , ' embed ' )
2023-12-08 17:13:38 +13:00
def profile_id ( self ) :
2023-12-11 20:46:38 +13:00
if self . ap_id :
return self . ap_id
else :
return f " https:// { current_app . config [ ' SERVER_NAME ' ] } /post/ { self . id } "
2023-12-08 17:13:38 +13:00
2024-06-05 16:23:31 +12:00
def public_url ( self ) :
return self . profile_id ( )
2024-01-11 20:39:22 +13:00
def blocked_by_content_filter ( self , content_filters ) :
lowercase_title = self . title . lower ( )
for name , keywords in content_filters . items ( ) if content_filters else { } :
for keyword in keywords :
if keyword in lowercase_title :
return name
return False
2024-09-30 16:34:17 +13:00
def posted_at_localized ( self , sort , locale ) :
# some locales do not have a definition for 'weeks' so are unable to display some dates in some languages. Fall back to english for those languages.
try :
return arrow . get ( self . last_active if sort == ' active ' else self . posted_at ) . humanize ( locale = locale )
except ValueError as v :
return arrow . get ( self . last_active if sort == ' active ' else self . posted_at ) . humanize ( locale = ' en ' )
2024-04-29 21:43:37 +12:00
def notify_new_replies ( self , user_id : int ) - > bool :
existing_notification = NotificationSubscription . query . filter ( NotificationSubscription . entity_id == self . id ,
NotificationSubscription . user_id == user_id ,
NotificationSubscription . type == NOTIF_POST ) . first ( )
return existing_notification is not None
2024-05-09 17:54:30 +12:00
def language_code ( self ) :
if self . language_id :
return self . language . code
else :
return ' en '
def language_name ( self ) :
if self . language_id :
return self . language . name
else :
return ' English '
2024-05-12 13:02:45 +12:00
def tags_for_activitypub ( self ) :
return_value = [ ]
for tag in self . tags :
return_value . append ( { ' type ' : ' Hashtag ' ,
' href ' : f ' https:// { current_app . config [ " SERVER_NAME " ] } /tag/ { tag . name } ' ,
' name ' : f ' # { tag . name } ' } )
return return_value
2024-09-28 13:05:00 +12:00
def post_reply_count_recalculate ( self ) :
self . post_reply_count = db . session . execute ( text ( ' SELECT COUNT(id) as c FROM " post_reply " WHERE post_id = :post_id AND deleted is false ' ) ,
{ ' post_id ' : self . id } ) . scalar ( )
2024-09-13 16:39:42 +12:00
# All the following post/comment ranking math is explained at https://medium.com/hacking-and-gonzo/how-reddit-ranking-algorithms-work-ef111e33d0d9
epoch = datetime ( 1970 , 1 , 1 )
def epoch_seconds ( self , date ) :
td = date - self . epoch
return td . days * 86400 + td . seconds + ( float ( td . microseconds ) / 1000000 )
# All the following post/comment ranking math is explained at https://medium.com/hacking-and-gonzo/how-reddit-ranking-algorithms-work-ef111e33d0d9
def post_ranking ( self , score , date : datetime ) :
if date is None :
date = datetime . utcnow ( )
if score is None :
score = 1
order = math . log ( max ( abs ( score ) , 1 ) , 10 )
sign = 1 if score > 0 else - 1 if score < 0 else 0
seconds = self . epoch_seconds ( date ) - 1685766018
return round ( sign * order + seconds / 45000 , 7 )
def vote ( self , user : User , vote_direction : str ) :
existing_vote = PostVote . query . filter_by ( user_id = user . id , post_id = self . id ) . first ( )
2024-09-26 15:45:41 +00:00
if existing_vote and vote_direction == ' reversal ' : # api sends '1' for upvote, '-1' for downvote, and '0' for reversal
if existing_vote . effect == 1 :
vote_direction = ' upvote '
elif existing_vote . effect == - 1 :
vote_direction = ' downvote '
assert vote_direction == ' upvote ' or vote_direction == ' downvote '
2024-09-13 16:39:42 +12:00
undo = None
if existing_vote :
if not self . community . low_quality :
self . author . reputation - = existing_vote . effect
if existing_vote . effect > 0 : # previous vote was up
if vote_direction == ' upvote ' : # new vote is also up, so remove it
db . session . delete ( existing_vote )
self . up_votes - = 1
2024-09-16 13:16:14 +00:00
self . score - = existing_vote . effect # score - (+1) = score-1
2024-09-13 16:39:42 +12:00
undo = ' Like '
else : # new vote is down while previous vote was up, so reverse their previous vote
existing_vote . effect = - 1
self . up_votes - = 1
self . down_votes + = 1
2024-09-16 13:16:14 +00:00
self . score + = existing_vote . effect * 2 # score + (-2) = score-2
2024-09-13 16:39:42 +12:00
else : # previous vote was down
if vote_direction == ' downvote ' : # new vote is also down, so remove it
db . session . delete ( existing_vote )
self . down_votes - = 1
2024-09-16 13:16:14 +00:00
self . score - = existing_vote . effect # score - (-1) = score+1
2024-09-13 16:39:42 +12:00
undo = ' Dislike '
else : # new vote is up while previous vote was down, so reverse their previous vote
existing_vote . effect = 1
self . up_votes + = 1
self . down_votes - = 1
2024-09-16 13:16:14 +00:00
self . score + = existing_vote . effect * 2 # score + (+2) = score+2
2024-09-21 10:54:29 +12:00
db . session . commit ( )
2024-09-13 16:39:42 +12:00
else :
if vote_direction == ' upvote ' :
effect = Instance . weight ( user . ap_domain )
spicy_effect = effect
# Make 'hot' sort more spicy by amplifying the effect of early upvotes
if self . up_votes + self . down_votes < = 10 :
spicy_effect = effect * current_app . config [ ' SPICY_UNDER_10 ' ]
elif self . up_votes + self . down_votes < = 30 :
spicy_effect = effect * current_app . config [ ' SPICY_UNDER_30 ' ]
elif self . up_votes + self . down_votes < = 60 :
spicy_effect = effect * current_app . config [ ' SPICY_UNDER_60 ' ]
if user . cannot_vote ( ) :
effect = spicy_effect = 0
self . up_votes + = 1
2024-09-16 13:16:14 +00:00
self . score + = spicy_effect # score + (+1) = score+1
2024-09-13 16:39:42 +12:00
else :
effect = - 1.0
2024-09-16 13:16:14 +00:00
spicy_effect = effect
2024-09-13 16:39:42 +12:00
self . down_votes + = 1
# Make 'hot' sort more spicy by amplifying the effect of early downvotes
if self . up_votes + self . down_votes < = 30 :
2024-09-16 13:16:14 +00:00
spicy_effect * = current_app . config [ ' SPICY_UNDER_30 ' ]
2024-09-13 16:39:42 +12:00
elif self . up_votes + self . down_votes < = 60 :
2024-09-16 13:16:14 +00:00
spicy_effect * = current_app . config [ ' SPICY_UNDER_60 ' ]
2024-09-13 16:39:42 +12:00
if user . cannot_vote ( ) :
2024-09-16 13:16:14 +00:00
effect = spicy_effect = 0
self . score + = spicy_effect # score + (-1) = score-1
2024-09-13 16:39:42 +12:00
vote = PostVote ( user_id = user . id , post_id = self . id , author_id = self . author . id ,
effect = effect )
# upvotes do not increase reputation in low quality communities
if self . community . low_quality and effect > 0 :
effect = 0
self . author . reputation + = effect
db . session . add ( vote )
2024-09-21 10:54:29 +12:00
user . last_seen = utcnow ( )
db . session . commit ( )
2024-09-13 16:39:42 +12:00
if not user . banned :
self . ranking = self . post_ranking ( self . score , self . created_at )
user . recalculate_attitude ( )
db . session . commit ( )
return undo
2023-08-05 21:26:24 +12:00
class PostReply ( db . Model ) :
2023-10-03 22:29:13 +13:00
query_class = FullTextSearchQuery
2023-08-05 21:26:24 +12:00
id = db . Column ( db . Integer , primary_key = True )
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
post_id = db . Column ( db . Integer , db . ForeignKey ( ' post.id ' ) , index = True )
community_id = db . Column ( db . Integer , db . ForeignKey ( ' community.id ' ) , index = True )
2023-10-10 22:25:37 +13:00
domain_id = db . Column ( db . Integer , db . ForeignKey ( ' domain.id ' ) , index = True )
2023-08-05 21:26:24 +12:00
image_id = db . Column ( db . Integer , db . ForeignKey ( ' file.id ' ) , index = True )
2024-02-09 15:14:39 +13:00
parent_id = db . Column ( db . Integer , index = True )
2023-08-05 21:26:24 +12:00
root_id = db . Column ( db . Integer )
2023-10-10 22:25:37 +13:00
depth = db . Column ( db . Integer , default = 0 )
2023-12-28 20:00:07 +13:00
instance_id = db . Column ( db . Integer , db . ForeignKey ( ' instance.id ' ) , index = True )
2023-08-05 21:26:24 +12:00
body = db . Column ( db . Text )
2023-08-10 21:13:37 +12:00
body_html = db . Column ( db . Text )
2023-09-16 19:09:04 +12:00
body_html_safe = db . Column ( db . Boolean , default = False )
2023-12-15 17:35:11 +13:00
score = db . Column ( db . Integer , default = 0 , index = True ) # used for 'top' sorting
2023-08-05 21:26:24 +12:00
nsfw = db . Column ( db . Boolean , default = False )
nsfl = db . Column ( db . Boolean , default = False )
2023-11-30 20:57:51 +13:00
notify_author = db . Column ( db . Boolean , default = True )
2023-12-12 08:53:35 +13:00
created_at = db . Column ( db . DateTime , index = True , default = utcnow )
posted_at = db . Column ( db . DateTime , index = True , default = utcnow )
2024-06-02 16:45:21 +12:00
deleted = db . Column ( db . Boolean , default = False , index = True )
2024-10-14 15:48:55 +13:00
deleted_by = db . Column ( db . Integer , index = True )
2023-08-05 21:26:24 +12:00
ip = db . Column ( db . String ( 50 ) )
2023-10-07 21:32:19 +13:00
from_bot = db . Column ( db . Boolean , default = False )
2023-08-05 21:26:24 +12:00
up_votes = db . Column ( db . Integer , default = 0 )
down_votes = db . Column ( db . Integer , default = 0 )
2024-01-07 21:36:04 +13:00
ranking = db . Column ( db . Float , default = 0.0 , index = True ) # used for 'hot' sorting
2024-05-09 17:54:30 +12:00
language_id = db . Column ( db . Integer , db . ForeignKey ( ' language.id ' ) , index = True )
2023-08-05 21:26:24 +12:00
edited_at = db . Column ( db . DateTime )
2023-12-17 00:12:49 +13:00
reports = db . Column ( db . Integer , default = 0 ) # how many times this post has been reported. Set to -1 to ignore reports
2023-08-05 21:26:24 +12:00
ap_id = db . Column ( db . String ( 255 ) , index = True )
2023-09-16 19:09:04 +12:00
ap_create_id = db . Column ( db . String ( 100 ) )
ap_announce_id = db . Column ( db . String ( 100 ) )
2023-08-05 21:26:24 +12:00
search_vector = db . Column ( TSVectorType ( ' body ' ) )
2023-12-27 16:58:30 +13:00
author = db . relationship ( ' User ' , lazy = ' joined ' , foreign_keys = [ user_id ] , single_parent = True , overlaps = " post_replies " )
2024-01-02 19:41:00 +13:00
community = db . relationship ( ' Community ' , lazy = ' joined ' , overlaps = ' replies ' , foreign_keys = [ community_id ] )
2024-05-09 17:54:30 +12:00
language = db . relationship ( ' Language ' , foreign_keys = [ language_id ] )
2024-09-28 13:05:00 +12:00
@classmethod
def new ( cls , user : User , post : Post , in_reply_to , body , body_html , notify_author , language_id , request_json : dict = None , announce_id = None ) :
from app . utils import shorten_string , blocked_phrases , recently_upvoted_post_replies , reply_already_exists , reply_is_just_link_to_gif_reaction , reply_is_stupid
from app . activitypub . util import notify_about_post_reply
if not post . comments_enabled :
raise Exception ( ' Comments are disabled on this post ' )
if in_reply_to is not None :
parent_id = in_reply_to . id
depth = in_reply_to . depth + 1
else :
parent_id = None
depth = 0
reply = PostReply ( user_id = user . id , post_id = post . id , parent_id = parent_id ,
depth = depth ,
community_id = post . community . id , body = body ,
body_html = body_html , body_html_safe = True ,
from_bot = user . bot , nsfw = post . nsfw , nsfl = post . nsfl ,
notify_author = notify_author , instance_id = user . instance_id ,
language_id = language_id ,
ap_id = request_json [ ' object ' ] [ ' id ' ] if request_json else None ,
ap_create_id = request_json [ ' id ' ] if request_json else None ,
ap_announce_id = announce_id )
if reply . body :
for blocked_phrase in blocked_phrases ( ) :
if blocked_phrase in reply . body :
raise Exception ( ' Blocked phrase in comment ' )
if in_reply_to is None or in_reply_to . parent_id is None :
notification_target = post
else :
notification_target = PostReply . query . get ( in_reply_to . parent_id )
if notification_target . author . has_blocked_user ( reply . user_id ) :
raise Exception ( ' Replier blocked ' )
if reply_already_exists ( user_id = user . id , post_id = post . id , parent_id = reply . parent_id , body = reply . body ) :
raise Exception ( ' Duplicate reply ' )
if reply_is_just_link_to_gif_reaction ( reply . body ) :
user . reputation - = 1
raise Exception ( ' Gif comment ignored ' )
if reply_is_stupid ( reply . body ) :
2024-10-14 15:37:00 +13:00
raise Exception ( ' Low quality reply ' )
2024-09-28 13:05:00 +12:00
db . session . add ( reply )
db . session . commit ( )
# Notify subscribers
notify_about_post_reply ( in_reply_to , reply )
# Subscribe to own comment
if notify_author :
new_notification = NotificationSubscription ( name = shorten_string ( _ ( ' Replies to my comment on %(post_title)s ' ,
post_title = post . title ) , 50 ) ,
user_id = user . id , entity_id = reply . id ,
type = NOTIF_REPLY )
db . session . add ( new_notification )
# upvote own reply
reply . score = 1
reply . up_votes = 1
reply . ranking = PostReply . confidence ( 1 , 0 )
vote = PostReplyVote ( user_id = user . id , post_reply_id = reply . id , author_id = user . id , effect = 1 )
db . session . add ( vote )
if user . is_local ( ) :
cache . delete_memoized ( recently_upvoted_post_replies , user . id )
reply . ap_id = reply . profile_id ( )
if user . reputation > 100 :
reply . up_votes + = 1
reply . score + = 1
reply . ranking + = 1
elif user . reputation < - 100 :
reply . score - = 1
reply . ranking - = 1
if not user . bot :
post . reply_count + = 1
post . community . post_reply_count + = 1
post . community . last_active = post . last_active = utcnow ( )
user . post_reply_count + = 1
db . session . commit ( )
return reply
2024-05-09 17:54:30 +12:00
def language_code ( self ) :
if self . language_id :
return self . language . code
else :
return ' en '
def language_name ( self ) :
if self . language_id :
return self . language . name
else :
return ' English '
2023-12-27 15:47:17 +13:00
2023-12-09 22:14:16 +13:00
def is_local ( self ) :
return self . ap_id is None or self . ap_id . startswith ( ' https:// ' + current_app . config [ ' SERVER_NAME ' ] )
2023-09-16 19:09:04 +12:00
@classmethod
def get_by_ap_id ( cls , ap_id ) :
return cls . query . filter_by ( ap_id = ap_id ) . first ( )
2023-12-08 17:13:38 +13:00
def profile_id ( self ) :
2023-12-10 15:10:09 +13:00
if self . ap_id :
return self . ap_id
else :
return f " https:// { current_app . config [ ' SERVER_NAME ' ] } /comment/ { self . id } "
2023-12-08 17:13:38 +13:00
2024-06-05 13:21:41 +12:00
def public_url ( self ) :
return self . profile_id ( )
2024-09-30 16:34:17 +13:00
def posted_at_localized ( self , locale ) :
try :
return arrow . get ( self . posted_at ) . humanize ( locale = locale )
except ValueError as v :
return arrow . get ( self . posted_at ) . humanize ( locale = ' en ' )
2023-12-09 22:14:16 +13:00
# the ap_id of the parent object, whether it's another PostReply or a Post
def in_reply_to ( self ) :
if self . parent_id is None :
return self . post . ap_id
else :
parent = PostReply . query . get ( self . parent_id )
return parent . ap_id
# the AP profile of the person who wrote the parent object, which could be another PostReply or a Post
def to ( self ) :
if self . parent_id is None :
2024-06-05 13:21:41 +12:00
return self . post . author . public_url ( )
2023-12-09 22:14:16 +13:00
else :
parent = PostReply . query . get ( self . parent_id )
2024-06-05 13:21:41 +12:00
return parent . author . public_url ( )
2023-12-09 22:14:16 +13:00
2023-12-26 12:36:02 +13:00
def delete_dependencies ( self ) :
2024-10-17 18:56:06 +00:00
"""
The first loop doesn ' t seem to ever be invoked with the current behaviour.
For replies with their own replies : functions which deal with removal don ' t set reply.deleted and don ' t call this , and
because reply . deleted isn ' t set, the cli task 7 days later doesn ' t call this either .
The plan is to set reply . deleted whether there ' s child replies or not (as happens with the API call), so I ' ve commented
it out so the current behaviour isn ' t changed.
2024-04-14 21:49:42 +12:00
for child_reply in self . child_replies ( ) :
child_reply . delete_dependencies ( )
db . session . delete ( child_reply )
2024-10-17 18:56:06 +00:00
"""
2024-04-14 21:49:42 +12:00
2024-06-22 07:49:14 +08:00
db . session . query ( PostReplyBookmark ) . filter ( PostReplyBookmark . post_reply_id == self . id ) . delete ( )
2023-12-26 12:36:02 +13:00
db . session . query ( Report ) . filter ( Report . suspect_post_reply_id == self . id ) . delete ( )
db . session . execute ( text ( ' DELETE FROM post_reply_vote WHERE post_reply_id = :post_reply_id ' ) ,
{ ' post_reply_id ' : self . id } )
if self . image_id :
file = File . query . get ( self . image_id )
file . delete_from_disk ( )
2024-04-14 21:49:42 +12:00
def child_replies ( self ) :
return PostReply . query . filter_by ( parent_id = self . id ) . all ( )
2023-12-26 12:36:02 +13:00
def has_replies ( self ) :
2024-06-02 16:45:21 +12:00
reply = PostReply . query . filter_by ( parent_id = self . id ) . filter ( PostReply . deleted == False ) . first ( )
2023-12-26 12:36:02 +13:00
return reply is not None
2024-01-11 20:39:22 +13:00
def blocked_by_content_filter ( self , content_filters ) :
lowercase_body = self . body . lower ( )
for name , keywords in content_filters . items ( ) if content_filters else { } :
for keyword in keywords :
if keyword in lowercase_body :
return name
return False
2024-04-29 21:43:37 +12:00
def notify_new_replies ( self , user_id : int ) - > bool :
existing_notification = NotificationSubscription . query . filter ( NotificationSubscription . entity_id == self . id ,
NotificationSubscription . user_id == user_id ,
NotificationSubscription . type == NOTIF_REPLY ) . first ( )
return existing_notification is not None
2024-09-13 16:39:42 +12:00
# used for ranking comments
2024-09-28 13:05:00 +12:00
@classmethod
def _confidence ( cls , ups , downs ) :
2024-09-13 16:39:42 +12:00
n = ups + downs
if n == 0 :
return 0.0
z = 1.281551565545
p = float ( ups ) / n
left = p + 1 / ( 2 * n ) * z * z
right = z * math . sqrt ( p * ( 1 - p ) / n + z * z / ( 4 * n * n ) )
under = 1 + 1 / n * z * z
return ( left - right ) / under
2024-09-28 13:05:00 +12:00
@classmethod
def confidence ( cls , ups , downs ) - > float :
2024-09-13 16:39:42 +12:00
if ups is None or ups < 0 :
ups = 0
if downs is None or downs < 0 :
downs = 0
if ups + downs == 0 :
return 0.0
else :
2024-09-28 13:05:00 +12:00
return cls . _confidence ( ups , downs )
2024-09-13 16:39:42 +12:00
def vote ( self , user : User , vote_direction : str ) :
existing_vote = PostReplyVote . query . filter_by ( user_id = user . id , post_reply_id = self . id ) . first ( )
2024-09-26 15:45:41 +00:00
if existing_vote and vote_direction == ' reversal ' : # api sends '1' for upvote, '-1' for downvote, and '0' for reversal
if existing_vote . effect == 1 :
vote_direction = ' upvote '
elif existing_vote . effect == - 1 :
vote_direction = ' downvote '
assert vote_direction == ' upvote ' or vote_direction == ' downvote '
2024-09-13 16:39:42 +12:00
undo = None
if existing_vote :
if existing_vote . effect > 0 : # previous vote was up
if vote_direction == ' upvote ' : # new vote is also up, so remove it
db . session . delete ( existing_vote )
self . up_votes - = 1
self . score - = 1
undo = ' Like '
else : # new vote is down while previous vote was up, so reverse their previous vote
existing_vote . effect = - 1
self . up_votes - = 1
self . down_votes + = 1
self . score - = 2
else : # previous vote was down
if vote_direction == ' downvote ' : # new vote is also down, so remove it
db . session . delete ( existing_vote )
self . down_votes - = 1
self . score + = 1
undo = ' Dislike '
else : # new vote is up while previous vote was down, so reverse their previous vote
existing_vote . effect = 1
self . up_votes + = 1
self . down_votes - = 1
self . score + = 2
else :
if user . cannot_vote ( ) :
effect = 0
else :
effect = 1
if vote_direction == ' upvote ' :
self . up_votes + = 1
else :
effect = effect * - 1
self . down_votes + = 1
self . score + = effect
vote = PostReplyVote ( user_id = user . id , post_reply_id = self . id , author_id = self . author . id ,
effect = effect )
self . author . reputation + = effect
db . session . add ( vote )
2024-11-03 10:47:41 +13:00
db . session . commit ( )
2024-09-13 16:48:39 +12:00
user . last_seen = utcnow ( )
2024-09-28 13:05:00 +12:00
self . ranking = PostReply . confidence ( self . up_votes , self . down_votes )
2024-09-13 16:39:42 +12:00
user . recalculate_attitude ( )
db . session . commit ( )
return undo
2023-08-05 21:26:24 +12:00
2024-09-27 10:18:49 -04:00
2023-08-05 21:26:24 +12:00
class Domain ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
name = db . Column ( db . String ( 255 ) , index = True )
post_count = db . Column ( db . Integer , default = 0 )
2023-09-16 19:09:04 +12:00
banned = db . Column ( db . Boolean , default = False , index = True ) # Domains can be banned site-wide (by admin) or DomainBlock'ed by users
2023-12-30 11:36:24 +13:00
notify_mods = db . Column ( db . Boolean , default = False , index = True )
notify_admins = db . Column ( db . Boolean , default = False , index = True )
2023-08-05 21:26:24 +12:00
2024-02-02 16:52:23 +13:00
def blocked_by ( self , user ) :
block = DomainBlock . query . filter_by ( domain_id = self . id , user_id = user . id ) . first ( )
return block is not None
def purge_content ( self ) :
files = File . query . join ( Post ) . filter ( Post . domain_id == self . id ) . all ( )
for file in files :
file . delete_from_disk ( )
posts = Post . query . filter_by ( domain_id = self . id ) . all ( )
for post in posts :
post . delete_dependencies ( )
db . session . delete ( post )
db . session . commit ( )
2023-08-05 21:26:24 +12:00
class DomainBlock ( db . Model ) :
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , primary_key = True )
domain_id = db . Column ( db . Integer , db . ForeignKey ( ' domain.id ' ) , primary_key = True )
2023-12-12 08:53:35 +13:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-08-05 21:26:24 +12:00
class CommunityBlock ( db . Model ) :
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , primary_key = True )
community_id = db . Column ( db . Integer , db . ForeignKey ( ' community.id ' ) , primary_key = True )
2023-12-12 08:53:35 +13:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-08-05 21:26:24 +12:00
class CommunityMember ( db . Model ) :
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , primary_key = True )
community_id = db . Column ( db . Integer , db . ForeignKey ( ' community.id ' ) , primary_key = True )
is_moderator = db . Column ( db . Boolean , default = False )
is_owner = db . Column ( db . Boolean , default = False )
2024-02-13 17:22:03 +13:00
is_banned = db . Column ( db . Boolean , default = False , index = True )
2024-01-07 12:47:06 +13:00
notify_new_posts = db . Column ( db . Boolean , default = False )
2023-12-12 08:53:35 +13:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-08-05 21:26:24 +12:00
2024-07-17 22:11:31 +08:00
class CommunityWikiPage ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
community_id = db . Column ( db . Integer , db . ForeignKey ( ' community.id ' ) , index = True )
slug = db . Column ( db . String ( 100 ) , index = True )
title = db . Column ( db . String ( 255 ) )
body = db . Column ( db . Text )
body_html = db . Column ( db . Text )
created_at = db . Column ( db . DateTime , default = utcnow )
edited_at = db . Column ( db . DateTime , default = utcnow )
who_can_edit = db . Column ( db . Integer , default = 0 ) # 0 = mods & admins, 1 = trusted, 2 = community members, 3 = anyone
revisions = db . relationship ( ' CommunityWikiPageRevision ' , backref = db . backref ( ' page ' ) , cascade = ' all,delete ' ,
lazy = ' dynamic ' )
def can_edit ( self , user : User , community : Community ) :
if user . is_anonymous :
return False
if self . who_can_edit == 0 :
if user . is_admin ( ) or user . is_staff ( ) or community . is_moderator ( user ) :
return True
elif self . who_can_edit == 1 :
if user . is_admin ( ) or user . is_staff ( ) or community . is_moderator ( user ) or user . trustworthy ( ) :
return True
elif self . who_can_edit == 2 :
if user . is_admin ( ) or user . is_staff ( ) or community . is_moderator ( user ) or user . trustworthy ( ) or community . is_member ( user ) :
return True
elif self . who_can_edit == 3 :
return True
return False
class CommunityWikiPageRevision ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
wiki_page_id = db . Column ( db . Integer , db . ForeignKey ( ' community_wiki_page.id ' ) , index = True )
community_id = db . Column ( db . Integer , db . ForeignKey ( ' community.id ' ) , index = True )
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) )
title = db . Column ( db . String ( 255 ) )
body = db . Column ( db . Text )
body_html = db . Column ( db . Text )
edited_at = db . Column ( db . DateTime , default = utcnow )
2024-07-18 15:14:55 +08:00
author = db . relationship ( ' User ' , lazy = ' joined ' , foreign_keys = [ user_id ] )
2024-07-17 22:11:31 +08:00
2024-04-29 16:13:29 +01:00
class UserFollower ( db . Model ) :
local_user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , primary_key = True )
remote_user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , primary_key = True )
is_accepted = db . Column ( db . Boolean , default = True ) # flip to ban remote user / reject follow
is_inward = db . Column ( db . Boolean , default = True ) # true = remote user is following a local one
created_at = db . Column ( db . DateTime , default = utcnow )
2023-09-09 20:46:40 +12:00
# people banned from communities
2023-09-08 20:04:01 +12:00
class CommunityBan ( db . Model ) :
2024-03-15 14:24:45 +13:00
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , primary_key = True ) # person who is banned, not the banner
2023-09-08 20:04:01 +12:00
community_id = db . Column ( db . Integer , db . ForeignKey ( ' community.id ' ) , primary_key = True )
banned_by = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) )
reason = db . Column ( db . String ( 50 ) )
2023-12-12 08:53:35 +13:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-09-08 20:04:01 +12:00
ban_until = db . Column ( db . DateTime )
2023-08-05 21:26:24 +12:00
class UserNote ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
2024-01-24 21:17:36 +13:00
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
target_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
2023-08-05 21:26:24 +12:00
body = db . Column ( db . Text )
2023-12-12 08:53:35 +13:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-08-05 21:26:24 +12:00
class UserBlock ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
2024-01-24 21:17:36 +13:00
blocker_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
blocked_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
2023-12-12 08:53:35 +13:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-08-05 21:26:24 +12:00
class Settings ( db . Model ) :
name = db . Column ( db . String ( 50 ) , primary_key = True )
value = db . Column ( db . String ( 1024 ) )
2023-09-05 20:25:02 +12:00
class Interest ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
name = db . Column ( db . String ( 50 ) )
communities = db . Column ( db . Text )
2023-09-08 20:04:01 +12:00
class CommunityJoinRequest ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) )
2024-01-24 21:17:36 +13:00
community_id = db . Column ( db . Integer , db . ForeignKey ( ' community.id ' ) , index = True )
2023-09-08 20:04:01 +12:00
class UserFollowRequest ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) )
follow_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) )
2024-02-02 15:30:03 +13:00
class UserRegistration ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
answer = db . Column ( db . String ( 512 ) )
status = db . Column ( db . Integer , default = 0 , index = True ) # 0 = unapproved, 1 = approved
created_at = db . Column ( db . DateTime , default = utcnow )
approved_at = db . Column ( db . DateTime )
approved_by = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) )
user = db . relationship ( ' User ' , foreign_keys = [ user_id ] , lazy = ' joined ' )
2023-09-10 20:20:53 +12:00
class PostVote ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
2024-01-24 21:17:36 +13:00
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
2024-11-15 16:42:08 +13:00
author_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
2024-01-24 21:17:36 +13:00
post_id = db . Column ( db . Integer , db . ForeignKey ( ' post.id ' ) , index = True )
2024-01-10 10:18:11 +13:00
effect = db . Column ( db . Float , index = True )
2023-12-12 08:53:35 +13:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-11-30 20:57:51 +13:00
post = db . relationship ( ' Post ' , foreign_keys = [ post_id ] )
2023-09-10 20:20:53 +12:00
class PostReplyVote ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
2024-01-24 21:17:36 +13:00
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True ) # who voted
2024-11-15 16:42:08 +13:00
author_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True ) # the author of the reply voted on - who's reputation is affected
2024-01-24 21:17:36 +13:00
post_reply_id = db . Column ( db . Integer , db . ForeignKey ( ' post_reply.id ' ) , index = True )
2023-09-10 20:20:53 +12:00
effect = db . Column ( db . Float )
2023-12-12 08:53:35 +13:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-09-10 20:20:53 +12:00
2023-09-09 20:46:40 +12:00
# save every activity to a log, to aid debugging
class ActivityPubLog ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
direction = db . Column ( db . String ( 3 ) ) # 'in' or 'out'
2024-01-19 07:45:48 +13:00
activity_id = db . Column ( db . String ( 256 ) , index = True )
2023-09-09 20:46:40 +12:00
activity_type = db . Column ( db . String ( 50 ) ) # e.g. 'Follow', 'Accept', 'Like', etc
activity_json = db . Column ( db . Text ) # the full json of the activity
result = db . Column ( db . String ( 10 ) ) # 'success' or 'failure'
exception_message = db . Column ( db . Text )
2023-12-12 08:53:35 +13:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-09-09 20:46:40 +12:00
2023-10-02 22:16:44 +13:00
class Filter ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
title = db . Column ( db . String ( 50 ) )
2024-01-11 20:39:22 +13:00
filter_home = db . Column ( db . Boolean , default = True )
2023-10-02 22:16:44 +13:00
filter_posts = db . Column ( db . Boolean , default = True )
filter_replies = db . Column ( db . Boolean , default = False )
hide_type = db . Column ( db . Integer , default = 0 ) # 0 = hide with warning, 1 = hide completely
2024-01-24 21:17:36 +13:00
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
2024-01-11 20:39:22 +13:00
expire_after = db . Column ( db . Date )
keywords = db . Column ( db . String ( 500 ) )
2023-10-02 22:16:44 +13:00
2024-01-11 20:39:22 +13:00
def keywords_string ( self ) :
if self . keywords is None or self . keywords == ' ' :
return ' '
2024-03-11 20:14:12 +13:00
split_keywords = [ kw . strip ( ) for kw in self . keywords . split ( ' \n ' ) ]
return ' , ' . join ( split_keywords )
2023-10-02 22:16:44 +13:00
2023-10-18 22:23:59 +13:00
class Role ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
name = db . Column ( db . String ( 50 ) )
weight = db . Column ( db . Integer , default = 0 )
permissions = db . relationship ( ' RolePermission ' )
class RolePermission ( db . Model ) :
role_id = db . Column ( db . Integer , db . ForeignKey ( ' role.id ' ) , primary_key = True )
permission = db . Column ( db . String , primary_key = True , index = True )
2023-10-02 22:16:44 +13:00
2023-11-30 20:57:51 +13:00
class Notification ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
title = db . Column ( db . String ( 50 ) )
url = db . Column ( db . String ( 512 ) )
read = db . Column ( db . Boolean , default = False )
2024-01-24 21:17:36 +13:00
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True ) # who the notification should go to
2023-12-30 11:36:24 +13:00
author_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) ) # the person who caused the notification to happen
2023-12-12 08:53:35 +13:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-11-30 20:57:51 +13:00
2023-12-13 21:04:11 +13:00
class Report ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
reasons = db . Column ( db . String ( 256 ) )
description = db . Column ( db . String ( 256 ) )
2024-03-26 22:18:05 +13:00
status = db . Column ( db . Integer , default = 0 ) # 0 = new, 1 = escalated to admin, 2 = being appealed, 3 = resolved, 4 = discarded
2024-02-19 15:01:53 +13:00
type = db . Column ( db . Integer , default = 0 ) # 0 = user, 1 = post, 2 = reply, 3 = community, 4 = conversation
2023-12-13 21:04:11 +13:00
reporter_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) )
2024-03-18 21:05:13 +13:00
suspect_community_id = db . Column ( db . Integer , db . ForeignKey ( ' community.id ' ) )
2023-12-13 21:04:11 +13:00
suspect_user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) )
suspect_post_id = db . Column ( db . Integer , db . ForeignKey ( ' post.id ' ) )
2023-12-26 12:36:02 +13:00
suspect_post_reply_id = db . Column ( db . Integer , db . ForeignKey ( ' post_reply.id ' ) )
2024-02-19 15:01:53 +13:00
suspect_conversation_id = db . Column ( db . Integer , db . ForeignKey ( ' conversation.id ' ) )
2024-03-18 21:05:13 +13:00
in_community_id = db . Column ( db . Integer , db . ForeignKey ( ' community.id ' ) )
2024-03-26 22:18:05 +13:00
source_instance_id = db . Column ( db . Integer , db . ForeignKey ( ' instance.id ' ) ) # the instance of the reporter. mostly used to distinguish between local (instance 1) and remote reports
2023-12-13 21:04:11 +13:00
created_at = db . Column ( db . DateTime , default = utcnow )
updated = db . Column ( db . DateTime , default = utcnow )
2024-01-02 16:07:41 +13:00
# textual representation of self.type
def type_text ( self ) :
2024-02-19 15:01:53 +13:00
types = ( ' User ' , ' Post ' , ' Comment ' , ' Community ' , ' Conversation ' )
2024-01-02 16:07:41 +13:00
if self . type is None :
return ' '
else :
return types [ self . type ]
def is_local ( self ) :
2024-03-27 10:42:36 +13:00
return self . source_instance_id == 1
2024-01-02 16:07:41 +13:00
2023-12-17 00:12:49 +13:00
2024-04-19 19:20:09 +12:00
class NotificationSubscription ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
name = db . Column ( db . String ( 256 ) ) # to avoid needing to look up the thing subscribed to via entity_id
type = db . Column ( db . Integer , default = 0 , index = True ) # see constants.py for possible values: NOTIF_*
entity_id = db . Column ( db . Integer , index = True ) # ID of the user, post, community, etc being subscribed to
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True ) # To whom this subscription belongs
created_at = db . Column ( db . DateTime , default = utcnow ) # Perhaps very old subscriptions can be automatically deleted
2024-05-16 20:43:03 +12:00
class Poll ( db . Model ) :
post_id = db . Column ( db . Integer , db . ForeignKey ( ' post.id ' ) , primary_key = True )
end_poll = db . Column ( db . DateTime )
2024-05-16 21:53:38 +12:00
mode = db . Column ( db . String ( 10 ) ) # 'single' or 'multiple' determines whether people can vote for one or multiple options
2024-05-16 20:43:03 +12:00
local_only = db . Column ( db . Boolean )
latest_vote = db . Column ( db . DateTime )
2024-05-18 19:41:20 +12:00
def has_voted ( self , user_id ) :
existing_vote = PollChoiceVote . query . filter ( PollChoiceVote . user_id == user_id , PollChoiceVote . post_id == self . post_id ) . first ( )
return existing_vote is not None
def vote_for_choice ( self , choice_id , user_id ) :
existing_vote = PollChoiceVote . query . filter ( PollChoiceVote . user_id == user_id ,
PollChoiceVote . choice_id == choice_id ) . first ( )
if not existing_vote :
new_vote = PollChoiceVote ( choice_id = choice_id , user_id = user_id , post_id = self . post_id )
db . session . add ( new_vote )
choice = PollChoice . query . get ( choice_id )
choice . num_votes + = 1
self . latest_vote = datetime . utcnow ( )
db . session . commit ( )
def total_votes ( self ) :
return db . session . execute ( text ( ' SELECT SUM(num_votes) as s FROM " poll_choice " WHERE post_id = :post_id ' ) ,
{ ' post_id ' : self . post_id } ) . scalar ( )
2024-05-16 20:43:03 +12:00
class PollChoice ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
post_id = db . Column ( db . Integer , db . ForeignKey ( ' post.id ' ) , index = True )
choice_text = db . Column ( db . String ( 200 ) )
sort_order = db . Column ( db . Integer )
num_votes = db . Column ( db . Integer , default = 0 )
2024-05-18 19:41:20 +12:00
def percentage ( self , poll_total_votes ) :
return math . ceil ( self . num_votes / poll_total_votes * 100 )
2024-05-16 20:43:03 +12:00
class PollChoiceVote ( db . Model ) :
choice_id = db . Column ( db . Integer , db . ForeignKey ( ' poll_choice.id ' ) , primary_key = True )
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , primary_key = True )
post_id = db . Column ( db . Integer , db . ForeignKey ( ' post.id ' ) , index = True )
created_at = db . Column ( db . DateTime , default = utcnow )
2024-06-20 21:51:43 +08:00
class PostBookmark ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
post_id = db . Column ( db . Integer , db . ForeignKey ( ' post.id ' ) , index = True )
created_at = db . Column ( db . DateTime , default = utcnow )
class PostReplyBookmark ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
post_reply_id = db . Column ( db . Integer , db . ForeignKey ( ' post_reply.id ' ) , index = True )
created_at = db . Column ( db . DateTime , default = utcnow )
2024-07-06 14:50:49 +08:00
class ModLog ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
community_id = db . Column ( db . Integer , db . ForeignKey ( ' community.id ' ) , index = True )
type = db . Column ( db . String ( 10 ) ) # 'mod' or 'admin'
action = db . Column ( db . String ( 30 ) ) # 'removing post', 'banning from community', etc
reason = db . Column ( db . String ( 512 ) )
link = db . Column ( db . String ( 512 ) )
link_text = db . Column ( db . String ( 512 ) )
public = db . Column ( db . Boolean , default = False )
created_at = db . Column ( db . DateTime , default = utcnow )
2024-07-07 15:01:52 +08:00
community = db . relationship ( ' Community ' , lazy = ' joined ' , foreign_keys = [ community_id ] )
author = db . relationship ( ' User ' , lazy = ' joined ' , foreign_keys = [ user_id ] )
action_map = {
' add_mod ' : _l ( ' Added moderator ' ) ,
' remove_mod ' : _l ( ' Removed moderator ' ) ,
' featured_post ' : _l ( ' Featured post ' ) ,
' unfeatured_post ' : _l ( ' Unfeatured post ' ) ,
' delete_post ' : _l ( ' Deleted post ' ) ,
' restore_post ' : _l ( ' Un-deleted post ' ) ,
' delete_post_reply ' : _l ( ' Deleted comment ' ) ,
' restore_post_reply ' : _l ( ' Un-deleted comment ' ) ,
' delete_community ' : _l ( ' Deleted community ' ) ,
' delete_user ' : _l ( ' Deleted account ' ) ,
' undelete_user ' : _l ( ' Restored account ' ) ,
' ban_user ' : _l ( ' Banned account ' ) ,
' unban_user ' : _l ( ' Un-banned account ' ) ,
}
def action_to_str ( self ) :
if self . action in self . action_map :
return self . action_map [ self . action ]
else :
return self . action
2024-07-06 14:50:49 +08:00
2023-12-30 19:03:44 +13:00
class IpBan ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
ip_address = db . Column ( db . String ( 50 ) , index = True )
notes = db . Column ( db . String ( 150 ) )
created_at = db . Column ( db . DateTime , default = utcnow )
2023-12-17 00:12:49 +13:00
class Site ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
name = db . Column ( db . String ( 256 ) )
description = db . Column ( db . String ( 256 ) )
icon_id = db . Column ( db . Integer , db . ForeignKey ( ' file.id ' ) )
sidebar = db . Column ( db . Text , default = ' ' )
legal_information = db . Column ( db . Text , default = ' ' )
public_key = db . Column ( db . Text )
private_key = db . Column ( db . Text )
enable_downvotes = db . Column ( db . Boolean , default = True )
allow_local_image_posts = db . Column ( db . Boolean , default = True )
remote_image_cache_days = db . Column ( db . Integer , default = 30 )
enable_nsfw = db . Column ( db . Boolean , default = False )
enable_nsfl = db . Column ( db . Boolean , default = False )
community_creation_admin_only = db . Column ( db . Boolean , default = False )
reports_email_admins = db . Column ( db . Boolean , default = True )
2024-02-02 15:30:03 +13:00
registration_mode = db . Column ( db . String ( 20 ) , default = ' Closed ' ) # possible values: Open, RequireApplication, Closed
2023-12-17 00:12:49 +13:00
application_question = db . Column ( db . Text , default = ' ' )
allow_or_block_list = db . Column ( db . Integer , default = 2 ) # 1 = allow list, 2 = block list
allowlist = db . Column ( db . Text , default = ' ' )
blocklist = db . Column ( db . Text , default = ' ' )
2024-03-22 12:22:19 +13:00
blocked_phrases = db . Column ( db . Text , default = ' ' ) # discard incoming content with these phrases
2024-03-22 14:35:51 +13:00
auto_decline_referrers = db . Column ( db . Text , default = ' rdrama.net \n ahrefs.com ' ) # automatically decline registration requests if the referrer is one of these
2023-12-17 00:12:49 +13:00
created_at = db . Column ( db . DateTime , default = utcnow )
updated = db . Column ( db . DateTime , default = utcnow )
last_active = db . Column ( db . DateTime , default = utcnow )
2024-01-13 11:12:31 +13:00
log_activitypub_json = db . Column ( db . Boolean , default = False )
2024-02-07 17:31:12 +13:00
default_theme = db . Column ( db . String ( 20 ) , default = ' ' )
2024-04-12 16:22:58 +12:00
contact_email = db . Column ( db . String ( 255 ) , default = ' ' )
2024-04-22 19:53:12 +02:00
about = db . Column ( db . Text , default = ' ' )
2024-06-14 18:03:47 +08:00
logo = db . Column ( db . String ( 40 ) , default = ' ' )
logo_152 = db . Column ( db . String ( 40 ) , default = ' ' )
logo_32 = db . Column ( db . String ( 40 ) , default = ' ' )
logo_16 = db . Column ( db . String ( 40 ) , default = ' ' )
2024-07-06 14:50:49 +08:00
show_inoculation_block = db . Column ( db . Boolean , default = True )
2023-12-17 00:12:49 +13:00
2023-12-30 11:36:24 +13:00
@staticmethod
def admins ( ) - > List [ User ] :
2024-06-27 15:19:32 +08:00
return User . query . filter_by ( deleted = False , banned = False ) . join ( user_role ) . filter ( user_role . c . role_id == ROLE_ADMIN ) . order_by ( User . id ) . all ( )
2024-06-26 16:24:15 +02:00
@staticmethod
def staff ( ) - > List [ User ] :
2024-06-27 15:19:32 +08:00
return User . query . filter_by ( deleted = False , banned = False ) . join ( user_role ) . filter ( user_role . c . role_id == ROLE_STAFF ) . order_by ( User . id ) . all ( )
2023-12-30 11:36:24 +13:00
2023-12-17 00:12:49 +13:00
2024-06-16 14:03:59 +08:00
#class IngressQueue(db.Model):
# id = db.Column(db.Integer, primary_key=True)
# waiting_for = db.Column(db.String(255), index=True) # The AP ID of the object we're waiting to be created before this Activity can be ingested
# activity_pub_log_id = db.Column(db.Integer, db.ForeignKey('activity_pub_log.id')) # The original Activity that failed because some target object does not exist
# ap_date_published = db.Column(db.DateTime, default=utcnow) # The value of the datePublished field on the Activity
# created_at = db.Column(db.DateTime, default=utcnow)
# expires = db.Column(db.DateTime, default=utcnow) # When to give up waiting and delete this row
#
#
2023-08-05 21:26:24 +12:00
@login.user_loader
def load_user ( id ) :
return User . query . get ( int ( id ) )