2024-10-13 19:37:00 -07:00
import html
2023-12-11 11:53:35 -08:00
from datetime import datetime , timedelta , date , timezone
2023-08-05 02:26:24 -07:00
from time import time
2024-10-13 19:37:00 -07:00
from typing import List , Union , Type
2024-09-06 16:33:20 -07:00
from urllib . parse import urlparse , parse_qs , urlencode , urlunparse
2023-09-17 02:19:51 -07:00
2024-09-29 20:34:17 -07:00
import arrow
2024-02-18 18:01:53 -08:00
from flask import current_app , escape , url_for , render_template_string
2023-11-29 09:36:08 -08:00
from flask_login import UserMixin , current_user
2024-05-21 03:20:08 -07:00
from sqlalchemy import or_ , text , desc
2023-08-05 02:26:24 -07: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-30 18:15:10 -07:00
from sqlalchemy . dialects . postgresql import ARRAY
from sqlalchemy . ext . mutable import MutableList
2023-10-03 02:29:13 -07:00
from flask_sqlalchemy import BaseQuery
from sqlalchemy_searchable import SearchQueryMixin
2024-10-13 19:37:00 -07:00
from app import db , login , cache , celery , httpx_client , constants
2023-08-05 02:26:24 -07:00
import jwt
2023-11-29 23:57:51 -08:00
import os
2024-05-18 00:41:20 -07:00
import math
2023-08-05 02:26:24 -07:00
2023-09-08 01:04:01 -07:00
from app . constants import SUBSCRIPTION_NONMEMBER , SUBSCRIPTION_MEMBER , SUBSCRIPTION_MODERATOR , SUBSCRIPTION_OWNER , \
2024-06-27 00:19:32 -07:00
SUBSCRIPTION_BANNED , SUBSCRIPTION_PENDING , NOTIF_USER , NOTIF_COMMUNITY , NOTIF_TOPIC , NOTIF_POST , NOTIF_REPLY , \
ROLE_ADMIN , ROLE_STAFF
2023-08-29 03:01:06 -07:00
2023-08-05 02:26:24 -07:00
2023-12-16 23:33:27 -08:00
# datetime.utcnow() is depreciated in Python 3.12 so it will need to be swapped out eventually
2023-12-11 11:53:35 -08:00
def utcnow ( ) :
2023-12-16 23:33:27 -08:00
return datetime . utcnow ( )
2023-12-11 11:53:35 -08:00
2023-10-03 02:29:13 -07:00
class FullTextSearchQuery ( BaseQuery , SearchQueryMixin ) :
pass
2024-01-02 19:29:58 -08: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-03 19:09:22 -08:00
dormant = db . Column ( db . Boolean , default = False ) # True once this instance is considered offline and not worth sending to any more
2024-01-02 19:29:58 -08:00
start_trying_again = db . Column ( db . DateTime ) # When to start trying again. Should grow exponentially with each failure.
2024-01-03 19:09:22 -08:00
gone_forever = db . Column ( db . Boolean , default = False ) # True once this instance is considered offline forever - never start trying again
2024-01-02 19:29:58 -08:00
ip_address = db . Column ( db . String ( 50 ) )
2024-02-22 19:52:17 -08:00
trusted = db . Column ( db . Boolean , default = False )
2024-04-18 01:51:08 -07:00
posting_warning = db . Column ( db . String ( 512 ) )
2024-05-23 06:19:23 -07:00
nodeinfo_href = db . Column ( db . String ( 100 ) )
2024-01-02 19:29:58 -08: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-18 15:18:23 -07:00
return not ( self . dormant or self . gone_forever )
2024-01-02 19:29:58 -08:00
2024-02-13 15:31:44 -08: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-19 12:03:08 -07:00
def votes_are_public ( self ) :
2024-09-23 17:04:29 -07:00
if self . trusted is True : # only vote privately with untrusted instances
return False
2024-10-10 07:39:36 -07:00
return self . software . lower ( ) == ' lemmy ' or self . software . lower ( ) == ' mbin ' or self . software . lower ( ) == ' kbin ' or self . software . lower ( ) == ' guppe groups '
2024-08-19 12:03:08 -07:00
2024-09-05 08:59:01 -07:00
def post_count ( self ) :
2024-09-05 21:00:09 -07: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 08:59:01 -07:00
def post_replies_count ( self ) :
2024-09-05 21:00:09 -07: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 08:59:01 -07:00
def known_communities_count ( self ) :
2024-09-05 21:00:09 -07: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 08:59:01 -07:00
2024-09-05 10:24:30 -07:00
def known_users_count ( self ) :
2024-09-05 21:00:09 -07: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 08:59:01 -07:00
2024-10-22 12:37:08 -07: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-12 21:39:42 -07: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-17 18:12:58 -07:00
def __repr__ ( self ) :
return ' <Instance {} > ' . format ( self . domain )
2024-09-03 09:47:26 -07:00
@classmethod
def unique_software_names ( cls ) :
return list ( db . session . execute ( text ( ' SELECT DISTINCT software FROM instance ORDER BY software ' ) ) . scalars ( ) )
2024-02-13 15:31:44 -08: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-02 19:29:58 -08: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-18 18:01:53 -08: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-18 18:56:56 -08: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-18 18:01:53 -08: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 02:23:19 -07:00
class Tag ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
2024-05-10 18:45:04 -07: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-11 18:02:45 -07:00
post_count = db . Column ( db . Integer , default = 0 )
banned = db . Column ( db . Boolean , default = False , index = True )
2024-04-16 02:23:19 -07:00
2024-11-01 19:14:31 -07:00
class Licence ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
name = db . Column ( db . String ( 50 ) )
2024-04-16 02:23:19 -07: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 02:26:24 -07: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 03:44:43 -07:00
alt_text = db . Column ( db . String ( 1500 ) )
2024-02-12 09:50:25 -08:00
source_url = db . Column ( db . String ( 1024 ) )
2023-11-27 01:05:35 -08:00
thumbnail_path = db . Column ( db . String ( 255 ) )
thumbnail_width = db . Column ( db . Integer )
thumbnail_height = db . Column ( db . Integer )
2024-05-05 20:42:12 -07:00
def view_url ( self , resize = False ) :
2023-11-27 01:05:35 -08:00
if self . source_url :
2024-05-05 20:42:12 -07: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 01:05:35 -08:00
elif self . file_path :
file_path = self . file_path [ 4 : ] if self . file_path . startswith ( ' app/ ' ) else self . file_path
return f " https:// { current_app . config [ ' SERVER_NAME ' ] } / { file_path } "
else :
return ' '
2024-01-25 00:18:44 -08: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
return f " https:// { current_app . config [ ' SERVER_NAME ' ] } / { file_path } "
2023-11-27 01:05:35 -08:00
def thumbnail_url ( self ) :
2023-12-21 01:14:43 -08:00
if self . thumbnail_path is None :
if self . source_url :
return self . source_url
else :
return ' '
2023-11-27 01:05:35 -08:00
thumbnail_path = self . thumbnail_path [ 4 : ] if self . thumbnail_path . startswith ( ' app/ ' ) else self . thumbnail_path
return f " https:// { current_app . config [ ' SERVER_NAME ' ] } / { thumbnail_path } "
2023-08-05 02:26:24 -07:00
2023-11-29 23:57:51 -08:00
def delete_from_disk ( self ) :
2024-03-22 19:12:51 -07:00
purge_from_cache = [ ]
2023-11-29 23:57:51 -08:00
if self . file_path and os . path . isfile ( self . file_path ) :
2024-04-06 14:39:50 -07:00
try :
os . unlink ( self . file_path )
except FileNotFoundError as e :
. . .
2024-03-22 19:12:51 -07:00
purge_from_cache . append ( self . file_path . replace ( ' app/ ' , f " https:// { current_app . config [ ' SERVER_NAME ' ] } / " ) )
2023-11-29 23:57:51 -08:00
if self . thumbnail_path and os . path . isfile ( self . thumbnail_path ) :
2024-04-06 14:39:50 -07:00
try :
os . unlink ( self . thumbnail_path )
except FileNotFoundError as e :
. . .
2024-03-22 19:12:51 -07:00
purge_from_cache . append ( self . thumbnail_path . replace ( ' app/ ' , f " https:// { current_app . config [ ' SERVER_NAME ' ] } / " ) )
2024-04-03 00:13:05 -07: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-06 14:39:50 -07:00
try :
os . unlink ( self . source_url . replace ( f " https:// { current_app . config [ ' SERVER_NAME ' ] } / " , ' app/ ' ) )
except FileNotFoundError as e :
. . .
2024-04-03 00:13:05 -07:00
purge_from_cache . append ( self . source_url ) # otoh it makes purging the cdn cache super easy.
2024-03-22 19:12:51 -07:00
if purge_from_cache :
flush_cdn_cache ( purge_from_cache )
2023-11-29 23:57:51 -08:00
2024-02-09 14:42:18 -08: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 02:26:24 -07:00
2024-03-22 19:12:51 -07: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 00:30:45 -07:00
response = httpx_client . request (
2024-03-22 19:12:51 -07:00
' POST ' ,
f ' https://api.cloudflare.com/client/v4/zones/ { zone_id } /purge_cache ' ,
headers = headers ,
json = body ,
timeout = 5 ,
)
2024-01-03 19:00:19 -08:00
class Topic ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
2024-01-27 21:11:32 -08:00
machine_name = db . Column ( db . String ( 50 ) , index = True )
2024-01-03 19:00:19 -08:00
name = db . Column ( db . String ( 50 ) )
num_communities = db . Column ( db . Integer , default = 0 )
2024-02-29 23:32:29 -08:00
parent_id = db . Column ( db . Integer )
2024-11-07 18:09:24 -08:00
show_posts_in_children = db . Column ( db . Boolean , default = False )
2024-01-03 19:00:19 -08:00
communities = db . relationship ( ' Community ' , lazy = ' dynamic ' , backref = ' topic ' , cascade = " all, delete-orphan " )
2024-04-08 01:01:08 -07: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-28 21:03:00 -07: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 02:26:24 -07:00
class Community ( db . Model ) :
2023-10-03 02:29:13 -07:00
query_class = FullTextSearchQuery
2023-08-05 02:26:24 -07:00
id = db . Column ( db . Integer , primary_key = True )
icon_id = db . Column ( db . Integer , db . ForeignKey ( ' file.id ' ) )
2023-08-29 03:01:06 -07:00
image_id = db . Column ( db . Integer , db . ForeignKey ( ' file.id ' ) )
2023-10-20 19:49:01 -07:00
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) )
2023-08-05 02:26:24 -07:00
name = db . Column ( db . String ( 256 ) , index = True )
title = db . Column ( db . String ( 256 ) )
2023-12-21 01:14:43 -08:00
description = db . Column ( db . Text ) # markdown
description_html = db . Column ( db . Text ) # html equivalent of above markdown
2023-08-05 02:26:24 -07:00
rules = db . Column ( db . Text )
2023-12-21 01:14:43 -08:00
rules_html = db . Column ( db . Text )
2023-12-13 00:04:11 -08:00
content_warning = db . Column ( db . Text ) # "Are you sure you want to view this community?"
2023-08-05 02:26:24 -07: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 00:04:11 -08: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-11 11:53:35 -08:00
created_at = db . Column ( db . DateTime , default = utcnow )
last_active = db . Column ( db . DateTime , default = utcnow )
2023-08-05 02:26:24 -07:00
public_key = db . Column ( db . Text )
private_key = db . Column ( db . Text )
2023-12-30 15:09:20 -08:00
content_retention = db . Column ( db . Integer , default = - 1 )
2024-01-03 19:00:19 -08:00
topic_id = db . Column ( db . Integer , db . ForeignKey ( ' topic.id ' ) , index = True )
2024-01-20 18:44:13 -08:00
default_layout = db . Column ( db . String ( 15 ) )
2024-04-18 01:51:08 -07:00
posting_warning = db . Column ( db . String ( 512 ) )
2023-08-05 02:26:24 -07:00
ap_id = db . Column ( db . String ( 255 ) , index = True )
2023-08-22 02:24:11 -07:00
ap_profile_id = db . Column ( db . String ( 255 ) , index = True )
2023-08-05 02:26:24 -07: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-22 19:52:17 -08:00
ap_outbox_url = db . Column ( db . String ( 255 ) )
2024-03-19 00:34:19 -07:00
ap_featured_url = db . Column ( db . String ( 255 ) )
2023-12-03 01:41:15 -08:00
ap_moderators_url = db . Column ( db . String ( 255 ) )
2023-08-05 02:26:24 -07:00
ap_domain = db . Column ( db . String ( 255 ) )
banned = db . Column ( db . Boolean , default = False )
2023-08-10 02:13:37 -07:00
restricted_to_mods = db . Column ( db . Boolean , default = False )
2024-01-01 22:41:00 -08:00
local_only = db . Column ( db . Boolean , default = False ) # only users on this instance can post
2023-12-13 00:04:11 -08:00
new_mods_wanted = db . Column ( db . Boolean , default = False )
2023-08-05 02:26:24 -07:00
searchable = db . Column ( db . Boolean , default = True )
2023-09-05 01:25:02 -07:00
private_mods = db . Column ( db . Boolean , default = False )
2023-08-05 02:26:24 -07:00
2023-12-30 15:09:20 -08: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 02:07:22 -07:00
ignore_remote_language = db . Column ( db . Boolean , default = False )
2023-08-29 03:01:06 -07:00
search_vector = db . Column ( TSVectorType ( ' name ' , ' title ' , ' description ' , ' rules ' ) )
2023-08-05 02:26:24 -07:00
2024-01-01 22:41:00 -08:00
posts = db . relationship ( ' Post ' , lazy = ' dynamic ' , cascade = " all, delete-orphan " )
replies = db . relationship ( ' PostReply ' , lazy = ' dynamic ' , cascade = " all, delete-orphan " )
2024-07-17 07:11:31 -07:00
wiki_pages = db . relationship ( ' CommunityWikiPage ' , lazy = ' dynamic ' , backref = ' community ' , cascade = " all, delete-orphan " )
2023-08-29 03:01:06 -07: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 02:23:19 -07:00
languages = db . relationship ( ' Language ' , lazy = ' dynamic ' , secondary = community_language , backref = db . backref ( ' communities ' , lazy = ' dynamic ' ) )
2023-08-29 03:01:06 -07:00
2024-05-08 02:07:22 -07:00
def language_ids ( self ) :
return [ language . id for language in self . languages . all ( ) ]
2023-11-29 10:12:17 -08:00
@cache.memoize ( timeout = 500 )
2023-12-07 20:13:38 -08:00
def icon_image ( self , size = ' default ' ) - > str :
2023-08-29 03:01:06 -07:00
if self . icon_id is not None :
2023-12-07 20:13:38 -08: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-04 19:41:50 -08:00
return ' /static/images/1px.gif '
2023-08-29 03:01:06 -07:00
2023-11-29 10:12:17 -08:00
@cache.memoize ( timeout = 500 )
2023-08-29 03:01:06 -07:00
def header_image ( self ) - > str :
if self . image_id is not None :
if self . image . file_path is not None :
2023-12-07 20:13:38 -08:00
if self . image . file_path . startswith ( ' app/ ' ) :
return self . image . file_path . replace ( ' app/ ' , ' / ' )
else :
return self . image . file_path
2023-08-29 03:01:06 -07:00
if self . image . source_url is not None :
2023-12-07 20:13:38 -08:00
if self . image . source_url . startswith ( ' app/ ' ) :
return self . image . source_url . replace ( ' app/ ' , ' / ' )
else :
return self . image . source_url
2023-08-29 03:01:06 -07: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 00:39:56 -08:00
return self . ap_id . lower ( )
2023-08-10 02:13:37 -07:00
2024-03-12 20:40:20 -07:00
@cache.memoize ( timeout = 3 )
2023-09-17 02:19:51 -07:00
def moderators ( self ) :
return CommunityMember . query . filter ( ( CommunityMember . community_id == self . id ) &
( or_ (
CommunityMember . is_owner ,
CommunityMember . is_moderator
2024-04-05 20:29:47 -07:00
) )
) . filter ( CommunityMember . is_banned == False ) . all ( )
2023-09-17 02:19:51 -07:00
2024-07-17 07:11:31 -07: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-25 15:36:02 -08:00
def is_moderator ( self , user = None ) :
if user is None :
2024-07-17 07:11:31 -07:00
return any ( moderator . user_id == current_user . get_id ( ) for moderator in self . moderators ( ) )
2023-12-25 15:36:02 -08:00
else :
return any ( moderator . user_id == user . id for moderator in self . moderators ( ) )
2023-11-29 09:36:08 -08:00
2023-12-25 15:36:02 -08:00
def is_owner ( self , user = None ) :
if user is None :
2024-07-17 07:11:31 -07:00
return any ( moderator . user_id == current_user . get_id ( ) and moderator . is_owner for moderator in self . moderators ( ) )
2023-12-25 15:36:02 -08:00
else :
return any ( moderator . user_id == user . id and moderator . is_owner for moderator in self . moderators ( ) )
2023-12-21 01:14:43 -08:00
2024-02-13 15:31:44 -08: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-06 15:47:06 -08:00
def user_is_banned ( self , user ) :
2024-04-28 21:03:00 -07:00
# use communities_banned_from() instead of this method, where possible. Redis caches the result of communities_banned_from()
2024-04-28 21:08:35 -07:00
# we cannot use communities_banned_from() in models.py because it causes a circular import
2024-04-28 21:03:00 -07: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-06 15:47:06 -08:00
2023-12-07 20:13:38 -08:00
def profile_id ( self ) :
2024-03-04 00:39:56 -08: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-07 20:13:38 -08:00
2024-03-23 19:12:34 -07: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 01:14:16 -08: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 00:04:11 -08: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-06 15:47:06 -08:00
def notify_new_posts ( self , user_id : int ) - > bool :
2024-04-22 01:53:03 -07: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-06 15:47:06 -08:00
2024-01-02 19:29:58 -08: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 00:39:52 -08:00
2024-03-24 09:38:20 -07: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 02:31:04 -07:00
def loop_videos ( self ) - > bool :
return ' gifs ' in self . name
2023-12-21 01:14:43 -08: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 07:10:04 -07:00
2023-10-18 02:23:59 -07: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 07:18:49 -07: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-29 17:49:06 -07:00
db . Column ( ' interacted_at ' , db . DateTime , index = True , default = utcnow ) # this is when the content is interacted with
2024-09-27 07:18:49 -07:00
)
2023-10-18 02:23:59 -07:00
2024-09-29 17:49:06 -07:00
2023-08-05 02:26:24 -07:00
class User ( UserMixin , db . Model ) :
2023-10-03 02:29:13 -07:00
query_class = FullTextSearchQuery
2023-08-05 02:26:24 -07:00
id = db . Column ( db . Integer , primary_key = True )
2023-11-21 23:48:27 -08:00
user_name = db . Column ( db . String ( 255 ) , index = True )
2024-08-19 12:03:08 -07:00
alt_user_name = db . Column ( db . String ( 255 ) , index = True )
2023-12-31 17:49:15 -08:00
title = db . Column ( db . String ( 256 ) )
2023-08-05 02:26:24 -07: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-25 20:41:11 -07:00
verification_token = db . Column ( db . String ( 16 ) , index = True )
2023-08-05 02:26:24 -07:00
banned = db . Column ( db . Boolean , default = False )
deleted = db . Column ( db . Boolean , default = False )
2024-10-13 19:48:55 -07:00
deleted_by = db . Column ( db . Integer , index = True )
2023-12-21 01:14:43 -08:00
about = db . Column ( db . Text ) # markdown
about_html = db . Column ( db . Text ) # html
2023-08-05 02:26:24 -07:00
keywords = db . Column ( db . String ( 256 ) )
2023-12-28 00:00:26 -08:00
matrix_user_id = db . Column ( db . String ( 256 ) )
2024-06-28 08:04:06 -07:00
hide_nsfw = db . Column ( db . Integer , default = 1 )
hide_nsfl = db . Column ( db . Integer , default = 1 )
2023-12-11 11:53:35 -08:00
created = db . Column ( db . DateTime , default = utcnow )
last_seen = db . Column ( db . DateTime , default = utcnow , index = True )
2024-02-12 20:22:03 -08: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 02:26:24 -07:00
public_key = db . Column ( db . Text )
private_key = db . Column ( db . Text )
newsletter = db . Column ( db . Boolean , default = True )
2024-02-22 19:52:17 -08: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 02:26:24 -07:00
bounces = db . Column ( db . SmallInteger , default = 0 )
timezone = db . Column ( db . String ( 20 ) )
2023-09-10 01:20:53 -07:00
reputation = db . Column ( db . Float , default = 0.0 )
2023-12-13 00:04:11 -08: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-12 16:08:04 -07:00
post_count = db . Column ( db . Integer , default = 0 )
post_reply_count = db . Column ( db . Integer , default = 0 )
2023-08-05 02:26:24 -07: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-25 18:10:01 -07:00
indexable = db . Column ( db . Boolean , default = False )
2023-10-07 01:32:19 -07:00
bot = db . Column ( db . Boolean , default = False )
2024-06-28 04:22:15 -07:00
ignore_bots = db . Column ( db . Integer , default = 0 )
2023-11-30 02:21:37 -08:00
unread_notifications = db . Column ( db . Integer , default = 0 )
2023-12-29 22:03:44 -08:00
ip_address = db . Column ( db . String ( 50 ) )
2024-08-01 01:24:36 -07:00
ip_address_country = db . Column ( db . String ( 50 ) )
2023-12-21 01:14:43 -08:00
instance_id = db . Column ( db . Integer , db . ForeignKey ( ' instance.id ' ) , index = True )
2023-12-31 19:26:57 -08:00
reports = db . Column ( db . Integer , default = 0 ) # how many times this user has been reported.
2024-01-14 21:26:22 -08:00
default_sort = db . Column ( db . String ( 25 ) , default = ' hot ' )
2024-08-15 18:42:29 -07:00
default_filter = db . Column ( db . String ( 25 ) , default = ' subscribed ' )
2024-02-06 20:31:12 -08:00
theme = db . Column ( db . String ( 20 ) , default = ' ' )
2024-02-22 19:52:17 -08:00
referrer = db . Column ( db . String ( 256 ) )
2024-02-26 00:26:19 -08:00
markdown_editor = db . Column ( db . Boolean , default = False )
2024-05-08 18:59:52 -07: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 03:34:54 -07:00
reply_collapse_threshold = db . Column ( db . Integer , default = - 10 )
reply_hide_threshold = db . Column ( db . Integer , default = - 20 )
2023-08-05 02:26:24 -07:00
2023-12-10 23:46:38 -08: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-29 14:36:24 -08:00
instance = db . relationship ( ' Instance ' , lazy = ' joined ' , foreign_keys = [ instance_id ] )
2024-02-18 18:01:53 -08:00
conversations = db . relationship ( ' Conversation ' , lazy = ' dynamic ' , secondary = conversation_member , backref = db . backref ( ' members ' , lazy = ' joined ' ) )
2023-09-08 01:04:01 -07:00
ap_id = db . Column ( db . String ( 255 ) , index = True ) # e.g. username@server
2024-11-13 23:16:09 -08:00
ap_profile_id = db . Column ( db . String ( 255 ) , index = True , unique = True ) # e.g. https://server/u/username
2024-06-03 15:01:06 -07:00
ap_public_url = db . Column ( db . String ( 255 ) ) # e.g. https://server/u/UserName
2023-08-05 02:26:24 -07: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 16:26:03 -07:00
ap_manually_approves_followers = db . Column ( db . Boolean , default = False )
2023-08-05 02:26:24 -07:00
ap_deleted_at = db . Column ( db . DateTime )
ap_inbox_url = db . Column ( db . String ( 255 ) )
ap_domain = db . Column ( db . String ( 255 ) )
2024-02-29 23:32:29 -08:00
search_vector = db . Column ( TSVectorType ( ' user_name ' , ' about ' , ' keywords ' ) )
2023-08-05 02:26:24 -07:00
activity = db . relationship ( ' ActivityLog ' , backref = ' account ' , lazy = ' dynamic ' , cascade = " all, delete-orphan " )
2023-11-29 10:12:17 -08:00
posts = db . relationship ( ' Post ' , lazy = ' dynamic ' , cascade = " all, delete-orphan " )
2023-12-26 18:47:17 -08:00
post_replies = db . relationship ( ' PostReply ' , lazy = ' dynamic ' , cascade = " all, delete-orphan " )
2023-08-05 02:26:24 -07:00
2023-10-18 02:23:59 -07:00
roles = db . relationship ( ' Role ' , secondary = user_role , lazy = ' dynamic ' , cascade = " all, delete " )
2024-09-27 08:00:20 -07:00
hide_read_posts = db . Column ( db . Boolean , default = False )
2024-09-27 09:36:51 -07: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 07:18:49 -07:00
2023-08-05 02:26:24 -07:00
def __repr__ ( self ) :
2024-01-17 17:56:23 -08:00
return ' <User {} _ {} > ' . format ( self . user_name , self . id )
2023-08-05 02:26:24 -07: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-11 15:34:08 -08:00
def get_id ( self ) :
if self . is_authenticated :
return self . id
else :
2024-01-11 16:49:40 -08:00
return 0
2024-01-11 15:34:08 -08:00
2023-10-20 19:49:01 -07:00
def display_name ( self ) :
if self . deleted is False :
2023-12-31 17:49:15 -08:00
if self . title :
2024-10-23 01:35:44 -07:00
return self . title . strip ( )
2023-12-31 17:49:15 -08:00
else :
2024-10-23 01:35:44 -07:00
return self . user_name . strip ( )
2023-10-20 19:49:01 -07:00
else :
return ' [deleted] '
2024-05-25 23:24:13 -07:00
@cache.memoize ( timeout = 500 )
2023-12-23 19:20:18 -08: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-25 23:24:13 -07:00
@cache.memoize ( timeout = 500 )
2023-10-07 01:32:19 -07:00
def avatar_image ( self ) - > str :
if self . avatar_id is not None :
if self . avatar . file_path is not None :
2023-12-07 20:13:38 -08:00
if self . avatar . file_path . startswith ( ' app/ ' ) :
return self . avatar . file_path . replace ( ' app/ ' , ' / ' )
else :
return self . avatar . file_path
2023-10-07 01:32:19 -07:00
if self . avatar . source_url is not None :
2023-12-07 20:13:38 -08:00
if self . avatar . source_url . startswith ( ' app/ ' ) :
return self . avatar . source_url . replace ( ' app/ ' , ' / ' )
else :
return self . avatar . source_url
2023-10-07 01:32:19 -07:00
return ' '
2024-05-25 23:24:13 -07:00
@cache.memoize ( timeout = 500 )
2023-10-07 01:32:19 -07:00
def cover_image ( self ) - > str :
if self . cover_id is not None :
2024-02-09 09:41:24 -08: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-07 20:13:38 -08:00
else :
2024-02-09 09:41:24 -08:00
return self . cover . thumbnail_path
2023-10-07 01:32:19 -07:00
if self . cover . source_url is not None :
2023-12-07 20:13:38 -08:00
if self . cover . source_url . startswith ( ' app/ ' ) :
return self . cover . source_url . replace ( ' app/ ' , ' / ' )
else :
return self . cover . source_url
2023-10-07 01:32:19 -07:00
return ' '
2024-02-09 14:42:18 -08: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-19 12:03:08 -07:00
def vote_privately ( self ) :
return self . alt_user_name is not None and self . alt_user_name != ' '
2024-02-09 14:42:18 -08:00
def num_content ( self ) :
content = 0
2024-03-20 15:07:11 -07: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-09 14:42:18 -08:00
return content
2023-12-09 01:14:16 -08: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-01 18:30:03 -08: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-02 19:29:58 -08:00
@cache.memoize ( timeout = 30 )
2023-12-21 01:14:43 -08:00
def is_admin ( self ) :
for role in self . roles :
if role . name == ' Admin ' :
return True
return False
2024-07-07 00:01:52 -07: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 00:14:12 -07:00
def trustworthy ( self ) :
if self . is_admin ( ) :
return True
if self . created_recently ( ) or self . reputation < 100 :
return False
return True
2024-09-12 16:08:04 -07: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 02:25:37 -07:00
def link ( self ) - > str :
2023-12-09 01:14:16 -08:00
if self . is_local ( ) :
2023-10-10 02:25:37 -07:00
return self . user_name
else :
2024-03-04 00:46:23 -08:00
return self . ap_id
2023-10-10 02:25:37 -07:00
2023-12-09 01:14:16 -08:00
def followers_url ( self ) :
if self . ap_followers_url :
return self . ap_followers_url
else :
2024-06-04 18:21:41 -07:00
return self . public_url ( ) + ' /followers '
2023-12-09 01:14:16 -08:00
2024-10-12 14:53:47 -07: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 02:26:24 -07: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-02 21:30:20 -07:00
algorithm = ' HS256 ' )
2023-08-05 02:26:24 -07: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-11 11:53:35 -08:00
return self . expires < utcnow ( ) + timedelta ( weeks = 1 )
2023-08-05 02:26:24 -07:00
def is_expired ( self ) :
if self . expires is None :
return True
2023-12-11 11:53:35 -08:00
return self . expires < utcnow ( )
2023-08-05 02:26:24 -07:00
def expired_ages_ago ( self ) :
if self . expires is None :
return True
return self . expires < datetime ( 2019 , 9 , 1 )
2023-12-26 22:51:07 -08:00
def recalculate_attitude ( self ) :
2024-09-12 16:08:04 -07:00
upvotes = downvotes = 0
2024-11-01 20:34:16 -07: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-26 22:51:07 -08: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-11 16:47:04 -07: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-26 22:51:07 -08:00
2024-09-12 16:08:04 -07: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 01:41:15 -08:00
def subscribed ( self , community_id : int ) - > int :
if community_id is None :
2023-08-29 03:01:06 -07:00
return False
2023-12-03 01:41:15 -08:00
subscription : CommunityMember = CommunityMember . query . filter_by ( user_id = self . id , community_id = community_id ) . first ( )
2023-08-29 03:01:06 -07:00
if subscription :
2023-09-08 01:04:01 -07:00
if subscription . is_banned :
return SUBSCRIPTION_BANNED
elif subscription . is_owner :
2023-08-29 03:01:06 -07:00
return SUBSCRIPTION_OWNER
elif subscription . is_moderator :
return SUBSCRIPTION_MODERATOR
else :
return SUBSCRIPTION_MEMBER
else :
2023-12-03 01:41:15 -08: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 03:01:06 -07:00
2023-09-17 02:19:51 -07:00
def communities ( self ) - > List [ Community ] :
return Community . query . filter ( Community . banned == False ) . \
2024-01-27 21:11:32 -08:00
join ( CommunityMember ) . filter ( CommunityMember . is_banned == False , CommunityMember . user_id == self . id ) . all ( )
2023-09-17 02:19:51 -07:00
2023-11-17 01:02:44 -08:00
def profile_id ( self ) :
2024-06-03 14:44:10 -07: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 00:46:23 -08:00
return result
2023-11-17 01:02:44 -08:00
2024-08-19 12:03:08 -07: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-23 19:12:34 -07:00
return result
2023-11-29 08:14:22 -08:00
def created_recently ( self ) :
2024-06-26 02:22:31 -07:00
if self . is_admin ( ) :
return False
2023-12-11 11:53:35 -08:00
return self . created and self . created > utcnow ( ) - timedelta ( days = 7 )
2023-11-29 08:14:22 -08:00
2023-12-31 19:26:57 -08:00
def has_blocked_instance ( self , instance_id : int ) :
2023-12-26 00:39:52 -08:00
instance_block = InstanceBlock . query . filter_by ( user_id = self . id , instance_id = instance_id ) . first ( )
return instance_block is not None
2023-12-31 19:26:57 -08: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 02:26:24 -07: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 )
2023-12-31 17:49:15 -08: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-01 18:30:03 -08:00
if self . waiting_for_approval ( ) :
db . session . query ( UserRegistration ) . filter ( UserRegistration . user_id == self . id ) . delete ( )
2024-04-22 01:53:03 -07: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 00:41:20 -07:00
db . session . query ( PollChoiceVote ) . filter ( PollChoiceVote . user_id == self . id ) . delete ( )
2024-06-21 16:49:14 -07:00
db . session . query ( PostBookmark ) . filter ( PostBookmark . user_id == self . id ) . delete ( )
db . session . query ( PostReplyBookmark ) . filter ( PostReplyBookmark . user_id == self . id ) . delete ( )
2023-12-31 17:49:15 -08:00
2024-06-01 21:45:21 -07:00
def purge_content ( self , soft = True ) :
2023-11-30 02:21:37 -08:00
files = File . query . join ( Post ) . filter ( Post . user_id == self . id ) . all ( )
for file in files :
file . delete_from_disk ( )
2023-12-31 17:49:15 -08:00
self . delete_dependencies ( )
2024-01-08 23:44:08 -08:00
posts = Post . query . filter_by ( user_id = self . id ) . all ( )
for post in posts :
post . delete_dependencies ( )
2024-06-01 21:45:21 -07:00
if soft :
post . deleted = True
else :
db . session . delete ( post )
2024-04-14 02:49:42 -07:00
db . session . commit ( )
2024-02-08 15:52:16 -08:00
post_replies = PostReply . query . filter_by ( user_id = self . id ) . all ( )
for reply in post_replies :
2024-04-14 02:49:42 -07:00
reply . delete_dependencies ( )
2024-06-01 21:45:21 -07:00
if soft :
reply . deleted = True
else :
db . session . delete ( reply )
2024-01-08 23:44:08 -08:00
db . session . commit ( )
2023-10-20 19:49:01 -07:00
2024-03-23 19:19:49 -07: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 01:06:08 -07: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 01:13:02 -07: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 01:06:08 -07:00
2024-09-20 09:06:08 -07:00
def encode_jwt_token ( self ) :
2024-10-27 06:36:17 -07: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 09:06:08 -07:00
2024-09-27 07:18:49 -07:00
# mark a post as 'read' for this user
2024-09-27 09:36:51 -07:00
def mark_post_as_read ( self , post ) :
2024-09-27 07:18:49 -07:00
# check if its already marked as read, if not, mark it as read
2024-09-27 09:36:51 -07:00
if not self . has_read_post ( post ) :
self . read_post . append ( post )
2024-09-27 07:18:49 -07:00
# check if post has been read by this user
# returns true if the post has been read, false if not
2024-09-27 09:36:51 -07: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 07:18:49 -07:00
2023-10-20 19:49:01 -07:00
2023-08-05 02:26:24 -07: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-11 11:53:35 -08:00
timestamp = db . Column ( db . DateTime , index = True , default = utcnow )
2023-08-05 02:26:24 -07:00
class Post ( db . Model ) :
2023-10-03 02:29:13 -07:00
query_class = FullTextSearchQuery
2023-08-05 02:26:24 -07: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 00:04:11 -08:00
instance_id = db . Column ( db . Integer , db . ForeignKey ( ' instance.id ' ) , index = True )
2024-11-01 19:14:31 -07:00
licence_id = db . Column ( db . Integer , db . ForeignKey ( ' licence.id ' ) , index = True )
2023-08-05 02:26:24 -07: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 02:13:37 -07:00
body_html = db . Column ( db . Text )
2024-10-13 19:37:00 -07:00
type = db . Column ( db . Integer , default = constants . POST_TYPE_ARTICLE )
2024-08-08 00:04:50 -07:00
microblog = db . Column ( db . Boolean , default = False )
2023-09-16 00:09:04 -07:00
comments_enabled = db . Column ( db . Boolean , default = True )
2024-06-01 21:45:21 -07:00
deleted = db . Column ( db . Boolean , default = False , index = True )
2024-10-13 19:48:55 -07:00
deleted_by = db . Column ( db . Integer , index = True )
2023-12-14 00:22:46 -08:00
mea_culpa = db . Column ( db . Boolean , default = False )
2023-08-05 02:26:24 -07:00
has_embed = db . Column ( db . Boolean , default = False )
reply_count = db . Column ( db . Integer , default = 0 )
2023-12-14 20:35:11 -08:00
score = db . Column ( db . Integer , default = 0 , index = True ) # used for 'top' ranking
2024-02-12 20:22:03 -08:00
nsfw = db . Column ( db . Boolean , default = False , index = True )
nsfl = db . Column ( db . Boolean , default = False , index = True )
2023-08-05 02:26:24 -07:00
sticky = db . Column ( db . Boolean , default = False )
2023-11-29 23:57:51 -08:00
notify_author = db . Column ( db . Boolean , default = True )
2024-03-12 00:58:47 -07:00
indexable = db . Column ( db . Boolean , default = True )
2024-02-12 20:22:03 -08:00
from_bot = db . Column ( db . Boolean , default = False , index = True )
2023-12-11 11:53:35 -08: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 02:26:24 -07: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-12 20:22:03 -08:00
ranking = db . Column ( db . Integer , default = 0 , index = True ) # used for 'hot' ranking
2023-08-05 02:26:24 -07:00
edited_at = db . Column ( db . DateTime )
2023-12-16 03:12:49 -08:00
reports = db . Column ( db . Integer , default = 0 ) # how many times this post has been reported. Set to -1 to ignore reports
2024-05-08 22:54:30 -07:00
language_id = db . Column ( db . Integer , db . ForeignKey ( ' language.id ' ) , index = True )
2024-03-30 18:15:10 -07:00
cross_posts = db . Column ( MutableList . as_mutable ( ARRAY ( db . Integer ) ) )
2024-04-16 02:23:19 -07:00
tags = db . relationship ( ' Tag ' , lazy = ' dynamic ' , secondary = post_tag , backref = db . backref ( ' posts ' , lazy = ' dynamic ' ) )
2023-08-05 02:26:24 -07:00
ap_id = db . Column ( db . String ( 255 ) , index = True )
2023-08-10 02:13:37 -07:00
ap_create_id = db . Column ( db . String ( 100 ) )
ap_announce_id = db . Column ( db . String ( 100 ) )
2023-08-05 02:26:24 -07:00
search_vector = db . Column ( TSVectorType ( ' title ' , ' body ' ) )
2023-11-29 10:12:17 -08: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 01:41:15 -08:00
author = db . relationship ( ' User ' , lazy = ' joined ' , overlaps = ' posts ' , foreign_keys = [ user_id ] )
2024-01-01 22:41:00 -08:00
community = db . relationship ( ' Community ' , lazy = ' joined ' , overlaps = ' posts ' , foreign_keys = [ community_id ] )
2023-12-09 18:10:09 -08:00
replies = db . relationship ( ' PostReply ' , lazy = ' dynamic ' , backref = ' post ' )
2024-05-08 22:54:30 -07:00
language = db . relationship ( ' Language ' , foreign_keys = [ language_id ] )
2024-11-01 20:02:29 -07:00
licence = db . relationship ( ' Licence ' , foreign_keys = [ licence_id ] )
2023-08-10 02:13:37 -07:00
2024-09-27 09:36:51 -07: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 01:14:16 -08: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 00:09:04 -07:00
@classmethod
def get_by_ap_id ( cls , ap_id ) :
return cls . query . filter_by ( ap_id = ap_id ) . first ( )
2024-10-13 19:37:00 -07:00
@classmethod
def new ( cls , user : User , community : Community , request_json : dict , announce_id = None ) :
2024-10-20 00:21:30 -07:00
from app . activitypub . util import instance_weight , find_language_or_create , find_language , find_hashtag_or_create , \
2024-11-01 20:02:29 -07:00
find_licence_or_create , make_image_sizes , notify_about_post
2024-10-13 19:37:00 -07: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-21 23:51:37 -07:00
is_video_hosting_site , communities_banned_from
2024-10-13 19:37:00 -07: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 00:21:30 -07:00
microblog = microblog ,
posted_at = utcnow ( )
2024-10-13 19:37:00 -07: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
if ' image ' in request_json [ ' object ' ] and ' url ' in request_json [ ' object ' ] [ ' image ' ] :
image = File ( source_url = request_json [ ' object ' ] [ ' image ' ] [ ' url ' ] )
else :
image = File ( source_url = post . url )
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-01 20:02:29 -07:00
post . language = language
2024-10-13 19:37:00 -07: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-01 20:02:29 -07: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-13 19:37:00 -07: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 00:21:30 -07:00
post . ranking = post . post_ranking ( post . score , post . posted_at )
2024-10-13 19:37:00 -07: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 00:21:30 -07:00
post . ranking = post . post_ranking ( post . score , post . posted_at )
2024-10-13 19:37:00 -07: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 02:05:07 -08:00
def delete_dependencies ( self ) :
2024-06-21 16:49:14 -07:00
db . session . query ( PostBookmark ) . filter ( PostBookmark . post_id == self . id ) . delete ( )
2024-05-18 00:41:20 -07: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-16 03:12:49 -08:00
db . session . query ( Report ) . filter ( Report . suspect_post_id == self . id ) . delete ( )
2024-09-06 19:15:34 -07:00
db . session . execute ( text ( ' DELETE FROM " post_vote " WHERE post_id = :post_id ' ) , { ' post_id ' : self . id } )
2024-10-24 23:17:10 -07: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-25 21:46:51 -07: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-29 23:57:51 -08:00
if self . image_id :
file = File . query . get ( self . image_id )
file . delete_from_disk ( )
2023-11-21 02:05:07 -08:00
2024-09-06 17:36:21 -07:00
def youtube_embed ( self , rel = True ) - > str :
2023-11-28 23:32:07 -08:00
if self . url :
2024-09-06 16:33:20 -07: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-06 17:36:21 -07:00
if rel :
query_params [ ' rel ' ] = ' 0 '
2024-09-06 16:33:20 -07: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-06 17:36:21 -07:00
if rel :
query_params [ ' rel ' ] = ' 0 '
2024-09-06 16:33:20 -07:00
new_query = urlencode ( query_params , doseq = True )
return f ' { video_id } ? { new_query } '
return ' '
2023-11-28 23:32:07 -08:00
2024-10-05 11:03:58 -07: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-25 18:19:57 -07:00
def peertube_embed ( self ) :
if self . url :
return self . url . replace ( ' watch ' , ' embed ' )
2023-12-07 20:13:38 -08:00
def profile_id ( self ) :
2023-12-10 23:46:38 -08:00
if self . ap_id :
return self . ap_id
else :
return f " https:// { current_app . config [ ' SERVER_NAME ' ] } /post/ { self . id } "
2023-12-07 20:13:38 -08:00
2024-06-04 21:23:31 -07:00
def public_url ( self ) :
return self . profile_id ( )
2024-01-10 23:39:22 -08: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-29 20:34:17 -07: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 02:43:37 -07: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-08 22:54:30 -07: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-11 18:02:45 -07: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-27 18:05:00 -07: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-12 21:39:42 -07: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 08:45:41 -07: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-12 21:39:42 -07: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 06:16:14 -07:00
self . score - = existing_vote . effect # score - (+1) = score-1
2024-09-12 21:39:42 -07: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 06:16:14 -07:00
self . score + = existing_vote . effect * 2 # score + (-2) = score-2
2024-09-12 21:39:42 -07: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 06:16:14 -07:00
self . score - = existing_vote . effect # score - (-1) = score+1
2024-09-12 21:39:42 -07: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 06:16:14 -07:00
self . score + = existing_vote . effect * 2 # score + (+2) = score+2
2024-09-20 15:54:29 -07:00
db . session . commit ( )
2024-09-12 21:39:42 -07: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 06:16:14 -07:00
self . score + = spicy_effect # score + (+1) = score+1
2024-09-12 21:39:42 -07:00
else :
effect = - 1.0
2024-09-16 06:16:14 -07:00
spicy_effect = effect
2024-09-12 21:39:42 -07: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 06:16:14 -07:00
spicy_effect * = current_app . config [ ' SPICY_UNDER_30 ' ]
2024-09-12 21:39:42 -07:00
elif self . up_votes + self . down_votes < = 60 :
2024-09-16 06:16:14 -07:00
spicy_effect * = current_app . config [ ' SPICY_UNDER_60 ' ]
2024-09-12 21:39:42 -07:00
if user . cannot_vote ( ) :
2024-09-16 06:16:14 -07:00
effect = spicy_effect = 0
self . score + = spicy_effect # score + (-1) = score-1
2024-09-12 21:39:42 -07: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-20 15:54:29 -07:00
user . last_seen = utcnow ( )
db . session . commit ( )
2024-09-12 21:39:42 -07: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 02:26:24 -07:00
class PostReply ( db . Model ) :
2023-10-03 02:29:13 -07:00
query_class = FullTextSearchQuery
2023-08-05 02:26:24 -07: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 02:25:37 -07:00
domain_id = db . Column ( db . Integer , db . ForeignKey ( ' domain.id ' ) , index = True )
2023-08-05 02:26:24 -07:00
image_id = db . Column ( db . Integer , db . ForeignKey ( ' file.id ' ) , index = True )
2024-02-08 18:14:39 -08:00
parent_id = db . Column ( db . Integer , index = True )
2023-08-05 02:26:24 -07:00
root_id = db . Column ( db . Integer )
2023-10-10 02:25:37 -07:00
depth = db . Column ( db . Integer , default = 0 )
2023-12-27 23:00:07 -08:00
instance_id = db . Column ( db . Integer , db . ForeignKey ( ' instance.id ' ) , index = True )
2023-08-05 02:26:24 -07:00
body = db . Column ( db . Text )
2023-08-10 02:13:37 -07:00
body_html = db . Column ( db . Text )
2023-09-16 00:09:04 -07:00
body_html_safe = db . Column ( db . Boolean , default = False )
2023-12-14 20:35:11 -08:00
score = db . Column ( db . Integer , default = 0 , index = True ) # used for 'top' sorting
2023-08-05 02:26:24 -07:00
nsfw = db . Column ( db . Boolean , default = False )
nsfl = db . Column ( db . Boolean , default = False )
2023-11-29 23:57:51 -08:00
notify_author = db . Column ( db . Boolean , default = True )
2023-12-11 11:53:35 -08:00
created_at = db . Column ( db . DateTime , index = True , default = utcnow )
posted_at = db . Column ( db . DateTime , index = True , default = utcnow )
2024-06-01 21:45:21 -07:00
deleted = db . Column ( db . Boolean , default = False , index = True )
2024-10-13 19:48:55 -07:00
deleted_by = db . Column ( db . Integer , index = True )
2023-08-05 02:26:24 -07:00
ip = db . Column ( db . String ( 50 ) )
2023-10-07 01:32:19 -07:00
from_bot = db . Column ( db . Boolean , default = False )
2023-08-05 02:26:24 -07:00
up_votes = db . Column ( db . Integer , default = 0 )
down_votes = db . Column ( db . Integer , default = 0 )
2024-01-07 00:36:04 -08:00
ranking = db . Column ( db . Float , default = 0.0 , index = True ) # used for 'hot' sorting
2024-05-08 22:54:30 -07:00
language_id = db . Column ( db . Integer , db . ForeignKey ( ' language.id ' ) , index = True )
2023-08-05 02:26:24 -07:00
edited_at = db . Column ( db . DateTime )
2023-12-16 03:12:49 -08: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 02:26:24 -07:00
ap_id = db . Column ( db . String ( 255 ) , index = True )
2023-09-16 00:09:04 -07:00
ap_create_id = db . Column ( db . String ( 100 ) )
ap_announce_id = db . Column ( db . String ( 100 ) )
2023-08-05 02:26:24 -07:00
search_vector = db . Column ( TSVectorType ( ' body ' ) )
2023-12-26 19:58:30 -08:00
author = db . relationship ( ' User ' , lazy = ' joined ' , foreign_keys = [ user_id ] , single_parent = True , overlaps = " post_replies " )
2024-01-01 22:41:00 -08:00
community = db . relationship ( ' Community ' , lazy = ' joined ' , overlaps = ' replies ' , foreign_keys = [ community_id ] )
2024-05-08 22:54:30 -07:00
language = db . relationship ( ' Language ' , foreign_keys = [ language_id ] )
2024-09-27 18:05:00 -07: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-13 19:37:00 -07:00
raise Exception ( ' Low quality reply ' )
2024-09-27 18:05:00 -07: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-08 22:54:30 -07: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-26 18:47:17 -08:00
2023-12-09 01:14:16 -08: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 00:09:04 -07:00
@classmethod
def get_by_ap_id ( cls , ap_id ) :
return cls . query . filter_by ( ap_id = ap_id ) . first ( )
2023-12-07 20:13:38 -08:00
def profile_id ( self ) :
2023-12-09 18:10:09 -08:00
if self . ap_id :
return self . ap_id
else :
return f " https:// { current_app . config [ ' SERVER_NAME ' ] } /comment/ { self . id } "
2023-12-07 20:13:38 -08:00
2024-06-04 18:21:41 -07:00
def public_url ( self ) :
return self . profile_id ( )
2024-09-29 20:34:17 -07: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 01:14:16 -08: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-04 18:21:41 -07:00
return self . post . author . public_url ( )
2023-12-09 01:14:16 -08:00
else :
parent = PostReply . query . get ( self . parent_id )
2024-06-04 18:21:41 -07:00
return parent . author . public_url ( )
2023-12-09 01:14:16 -08:00
2023-12-25 15:36:02 -08:00
def delete_dependencies ( self ) :
2024-10-17 11:56:06 -07: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 02:49:42 -07:00
for child_reply in self . child_replies ( ) :
child_reply . delete_dependencies ( )
db . session . delete ( child_reply )
2024-10-17 11:56:06 -07:00
"""
2024-04-14 02:49:42 -07:00
2024-06-21 16:49:14 -07:00
db . session . query ( PostReplyBookmark ) . filter ( PostReplyBookmark . post_reply_id == self . id ) . delete ( )
2023-12-25 15:36:02 -08: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 02:49:42 -07:00
def child_replies ( self ) :
return PostReply . query . filter_by ( parent_id = self . id ) . all ( )
2023-12-25 15:36:02 -08:00
def has_replies ( self ) :
2024-06-01 21:45:21 -07:00
reply = PostReply . query . filter_by ( parent_id = self . id ) . filter ( PostReply . deleted == False ) . first ( )
2023-12-25 15:36:02 -08:00
return reply is not None
2024-01-10 23:39:22 -08: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 02:43:37 -07: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-12 21:39:42 -07:00
# used for ranking comments
2024-09-27 18:05:00 -07:00
@classmethod
def _confidence ( cls , ups , downs ) :
2024-09-12 21:39:42 -07: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-27 18:05:00 -07:00
@classmethod
def confidence ( cls , ups , downs ) - > float :
2024-09-12 21:39:42 -07: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-27 18:05:00 -07:00
return cls . _confidence ( ups , downs )
2024-09-12 21:39:42 -07: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 08:45:41 -07: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-12 21:39:42 -07: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-02 14:47:41 -07:00
db . session . commit ( )
2024-09-12 21:48:39 -07:00
user . last_seen = utcnow ( )
2024-09-27 18:05:00 -07:00
self . ranking = PostReply . confidence ( self . up_votes , self . down_votes )
2024-09-12 21:39:42 -07:00
user . recalculate_attitude ( )
db . session . commit ( )
return undo
2023-08-05 02:26:24 -07:00
2024-09-27 07:18:49 -07:00
2023-08-05 02:26:24 -07: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 00:09:04 -07: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-29 14:36:24 -08:00
notify_mods = db . Column ( db . Boolean , default = False , index = True )
notify_admins = db . Column ( db . Boolean , default = False , index = True )
2023-08-05 02:26:24 -07:00
2024-02-01 19:52:23 -08: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 02:26:24 -07: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-11 11:53:35 -08:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-08-05 02:26:24 -07: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-11 11:53:35 -08:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-08-05 02:26:24 -07: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-12 20:22:03 -08:00
is_banned = db . Column ( db . Boolean , default = False , index = True )
2024-01-06 15:47:06 -08:00
notify_new_posts = db . Column ( db . Boolean , default = False )
2023-12-11 11:53:35 -08:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-08-05 02:26:24 -07:00
2024-07-17 07:11:31 -07: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 00:14:55 -07:00
author = db . relationship ( ' User ' , lazy = ' joined ' , foreign_keys = [ user_id ] )
2024-07-17 07:11:31 -07:00
2024-04-29 08:13:29 -07: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 01:46:40 -07:00
# people banned from communities
2023-09-08 01:04:01 -07:00
class CommunityBan ( db . Model ) :
2024-03-14 18:24:45 -07:00
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , primary_key = True ) # person who is banned, not the banner
2023-09-08 01:04:01 -07: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-11 11:53:35 -08:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-09-08 01:04:01 -07:00
ban_until = db . Column ( db . DateTime )
2023-08-05 02:26:24 -07:00
class UserNote ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
2024-01-24 00:17:36 -08: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 02:26:24 -07:00
body = db . Column ( db . Text )
2023-12-11 11:53:35 -08:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-08-05 02:26:24 -07:00
class UserBlock ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
2024-01-24 00:17:36 -08: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-11 11:53:35 -08:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-08-05 02:26:24 -07:00
class Settings ( db . Model ) :
name = db . Column ( db . String ( 50 ) , primary_key = True )
value = db . Column ( db . String ( 1024 ) )
2023-09-05 01:25:02 -07: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 01:04:01 -07: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 00:17:36 -08:00
community_id = db . Column ( db . Integer , db . ForeignKey ( ' community.id ' ) , index = True )
2023-09-08 01:04:01 -07: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-01 18:30:03 -08: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 01:20:53 -07:00
class PostVote ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
2024-01-24 00:17:36 -08:00
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
2024-11-14 19:42:08 -08:00
author_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
2024-01-24 00:17:36 -08:00
post_id = db . Column ( db . Integer , db . ForeignKey ( ' post.id ' ) , index = True )
2024-01-09 13:18:11 -08:00
effect = db . Column ( db . Float , index = True )
2023-12-11 11:53:35 -08:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-11-29 23:57:51 -08:00
post = db . relationship ( ' Post ' , foreign_keys = [ post_id ] )
2023-09-10 01:20:53 -07:00
class PostReplyVote ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
2024-01-24 00:17:36 -08:00
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True ) # who voted
2024-11-14 19:42:08 -08: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 00:17:36 -08:00
post_reply_id = db . Column ( db . Integer , db . ForeignKey ( ' post_reply.id ' ) , index = True )
2023-09-10 01:20:53 -07:00
effect = db . Column ( db . Float )
2023-12-11 11:53:35 -08:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-09-10 01:20:53 -07:00
2023-09-09 01:46:40 -07: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-18 10:45:48 -08:00
activity_id = db . Column ( db . String ( 256 ) , index = True )
2023-09-09 01:46:40 -07: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-11 11:53:35 -08:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-09-09 01:46:40 -07:00
2023-10-02 02:16:44 -07:00
class Filter ( db . Model ) :
id = db . Column ( db . Integer , primary_key = True )
title = db . Column ( db . String ( 50 ) )
2024-01-10 23:39:22 -08:00
filter_home = db . Column ( db . Boolean , default = True )
2023-10-02 02:16:44 -07: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 00:17:36 -08:00
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True )
2024-01-10 23:39:22 -08:00
expire_after = db . Column ( db . Date )
keywords = db . Column ( db . String ( 500 ) )
2023-10-02 02:16:44 -07:00
2024-01-10 23:39:22 -08:00
def keywords_string ( self ) :
if self . keywords is None or self . keywords == ' ' :
return ' '
2024-03-11 00:14:12 -07:00
split_keywords = [ kw . strip ( ) for kw in self . keywords . split ( ' \n ' ) ]
return ' , ' . join ( split_keywords )
2023-10-02 02:16:44 -07:00
2023-10-18 02:23:59 -07: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 02:16:44 -07:00
2023-11-29 23:57:51 -08: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 00:17:36 -08:00
user_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) , index = True ) # who the notification should go to
2023-12-29 14:36:24 -08:00
author_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) ) # the person who caused the notification to happen
2023-12-11 11:53:35 -08:00
created_at = db . Column ( db . DateTime , default = utcnow )
2023-11-29 23:57:51 -08:00
2023-12-13 00:04:11 -08: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 02:18:05 -07:00
status = db . Column ( db . Integer , default = 0 ) # 0 = new, 1 = escalated to admin, 2 = being appealed, 3 = resolved, 4 = discarded
2024-02-18 18:01:53 -08:00
type = db . Column ( db . Integer , default = 0 ) # 0 = user, 1 = post, 2 = reply, 3 = community, 4 = conversation
2023-12-13 00:04:11 -08:00
reporter_id = db . Column ( db . Integer , db . ForeignKey ( ' user.id ' ) )
2024-03-18 01:05:13 -07:00
suspect_community_id = db . Column ( db . Integer , db . ForeignKey ( ' community.id ' ) )
2023-12-13 00:04:11 -08: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-25 15:36:02 -08:00
suspect_post_reply_id = db . Column ( db . Integer , db . ForeignKey ( ' post_reply.id ' ) )
2024-02-18 18:01:53 -08:00
suspect_conversation_id = db . Column ( db . Integer , db . ForeignKey ( ' conversation.id ' ) )
2024-03-18 01:05:13 -07:00
in_community_id = db . Column ( db . Integer , db . ForeignKey ( ' community.id ' ) )
2024-03-26 02:18:05 -07: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 00:04:11 -08:00
created_at = db . Column ( db . DateTime , default = utcnow )
updated = db . Column ( db . DateTime , default = utcnow )
2024-01-01 19:07:41 -08:00
# textual representation of self.type
def type_text ( self ) :
2024-02-18 18:01:53 -08:00
types = ( ' User ' , ' Post ' , ' Comment ' , ' Community ' , ' Conversation ' )
2024-01-01 19:07:41 -08:00
if self . type is None :
return ' '
else :
return types [ self . type ]
def is_local ( self ) :
2024-03-26 14:42:36 -07:00
return self . source_instance_id == 1
2024-01-01 19:07:41 -08:00
2023-12-16 03:12:49 -08:00
2024-04-19 00:20:09 -07: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 01:43:03 -07: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 02:53:38 -07:00
mode = db . Column ( db . String ( 10 ) ) # 'single' or 'multiple' determines whether people can vote for one or multiple options
2024-05-16 01:43:03 -07:00
local_only = db . Column ( db . Boolean )
latest_vote = db . Column ( db . DateTime )
2024-05-18 00:41:20 -07: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 01:43:03 -07: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 00:41:20 -07:00
def percentage ( self , poll_total_votes ) :
return math . ceil ( self . num_votes / poll_total_votes * 100 )
2024-05-16 01:43:03 -07: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 06:51:43 -07: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-05 23:50:49 -07: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 00:01:52 -07: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-05 23:50:49 -07:00
2023-12-29 22:03:44 -08: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-16 03:12:49 -08: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-01 18:30:03 -08:00
registration_mode = db . Column ( db . String ( 20 ) , default = ' Closed ' ) # possible values: Open, RequireApplication, Closed
2023-12-16 03:12:49 -08: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-21 16:22:19 -07:00
blocked_phrases = db . Column ( db . Text , default = ' ' ) # discard incoming content with these phrases
2024-03-21 18:35:51 -07: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-16 03:12:49 -08: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-12 14:12:31 -08:00
log_activitypub_json = db . Column ( db . Boolean , default = False )
2024-02-06 20:31:12 -08:00
default_theme = db . Column ( db . String ( 20 ) , default = ' ' )
2024-04-11 21:22:58 -07:00
contact_email = db . Column ( db . String ( 255 ) , default = ' ' )
2024-04-22 10:53:12 -07:00
about = db . Column ( db . Text , default = ' ' )
2024-06-14 03:03:47 -07: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-05 23:50:49 -07:00
show_inoculation_block = db . Column ( db . Boolean , default = True )
2023-12-16 03:12:49 -08:00
2023-12-29 14:36:24 -08:00
@staticmethod
def admins ( ) - > List [ User ] :
2024-06-27 00:19:32 -07: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 07:24:15 -07:00
@staticmethod
def staff ( ) - > List [ User ] :
2024-06-27 00:19:32 -07: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-29 14:36:24 -08:00
2023-12-16 03:12:49 -08:00
2024-06-15 23:03:59 -07: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 02:26:24 -07:00
@login.user_loader
def load_user ( id ) :
return User . query . get ( int ( id ) )