heartranked

git clone https://git.tarina.org/heartranked
Log | Files | Refs | README

server.py (83339B)


      1 #!/usr/bin/python3
      2 # -*- coding: utf-8 -*-
      3 #heartranked is FREEDOM SOFTWARE by King Robin Johannes
      4 #don't worry, information want's to be free. nobody can stop it. Bless.
      5 
      6 import time, datetime, os, sys
      7 
      8 file_dir = os.path.dirname(__file__)
      9 sys.path.append(file_dir)
     10 
     11 import json
     12 import requests
     13 import subprocess
     14 import web
     15 import hashlib
     16 import random
     17 import markdown
     18 import re
     19 import unicodedata
     20 import urllib
     21 from pathlib import Path
     22 from mutagen.flac import FLAC
     23 from mutagen.easyid3 import EasyID3
     24 from mutagen.oggvorbis import OggVorbis
     25 from PIL import Image
     26 from PIL import ImageSequence
     27 import settings
     28 
     29 urls = (
     30     '/?','heartranked',
     31     '/trust', 'trust',
     32     "/stats?", "stats",
     33     "/login?", "login",
     34     "/logout", "logout",
     35     "/invites?", "invites",
     36     "/users","users",
     37     "/like?","like",
     38     "/imageapi?","imageapi",
     39     "/u/(.*)?", "user",
     40     "/forgotpass?", "forgotpass",
     41     "/register?", "register",
     42     "/tuning?", "tuning",
     43     '/editor?', 'editor',
     44     '/save', 'savepost',
     45     '/upload/(.*)?', 'upload',
     46     '/rendered', 'rendered',
     47     '/uploads?', 'uploads',
     48     '/p?', 'pull',
     49     '/c?', 'config')
     50 
     51 #Load from settings
     52 webmaster = settings.webmaster
     53 baseurl = settings.baseurl
     54 sitename = settings.sitename
     55 siteslogan = settings.siteslogan
     56 siteurl = baseurl
     57 postadmin = settings.postadmin
     58 postadmin_signature = settings.postadmin_signature
     59 heart=settings.heart
     60 hearted=settings.hearted
     61 zipandship=settings.zipandship
     62 theme=settings.theme
     63 #load msg
     64 postadmin_msg_new=settings.postadmin_msg_new
     65 postadmin_msg_new_long=settings.postadmin_msg_new_long
     66 welcome=settings.welcome
     67 welcome_long=settings.welcome_long
     68 basedir = os.path.dirname(os.path.realpath(__file__))+'/'
     69 templatedir = basedir + 'html/'
     70 staticdir = basedir + 'static/'
     71 web.config.debug = False
     72 app = web.application(urls, globals())
     73 store = web.session.DiskStore(basedir + 'sessions')
     74 render = web.template.render(templatedir, base="base")
     75 renderop = web.template.render(templatedir, base="op")
     76 rendersplash = web.template.render(templatedir, base="splash")
     77 session = web.session.Session(app,store,initializer={'login':0, 'privilege':0, 'bag':[], 'sessionkey':'empty','postid':'','backurl':'','user':'','search':'', 'bildsida':'', 'feedbase':'', 'timebase':'', 'usrfeed':'', 'saveto':'', 'theme':''})
     78 
     79 allowedchar = 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'
     80 
     81 datetimeformat="%Y-%m-%d %H:%M:%S"
     82 
     83 # --- Parameters (often called N, r, p) ---
     84 # N (n): CPU/Memory cost factor. Must be a power of 2 (e.g., 2**14 = 16384).
     85 # r: Block size factor (typically 8).
     86 # p: Parallelization factor (typically 1).
     87 # klen: Desired key length (e.g., 32 for a 256-bit key).
     88 N_COST = 16384  # Adjust this for desired security/speed tradeoff
     89 R_BLOCK_SIZE = 8
     90 P_PARALLELIZATION = 1
     91 KEY_LENGTH = 32
     92 
     93 def runfirst():
     94     os.makedirs(basedir+'p/posts',exist_ok=True)
     95     os.makedirs(basedir+'p/zipped',exist_ok=True)
     96     os.makedirs(basedir+'p/comborank',exist_ok=True)
     97     os.makedirs(basedir+'p/heartrank',exist_ok=True)
     98     os.makedirs(basedir+'p/deleted',exist_ok=True)
     99     os.makedirs(basedir+'u/',exist_ok=True)
    100     os.makedirs(basedir+'r/',exist_ok=True)
    101     os.makedirs(basedir+'r/visitors',exist_ok=True)
    102     os.makedirs(basedir+'r/invites',exist_ok=True)
    103     os.makedirs(basedir+'r/trusted',exist_ok=True)
    104     os.makedirs(basedir+'r/users',exist_ok=True)
    105     os.makedirs(basedir+'r/stopflood',exist_ok=True)
    106     os.makedirs(basedir+'r/stopresetpass',exist_ok=True)
    107 
    108 runfirst()
    109 
    110 def hash_password(password: str) -> bytes:
    111     # 1. Generate a random, unique salt for each password
    112     salt = os.urandom(16) 
    113     # 2. Derive the key (hash)
    114     # The 'password' must be bytes, so we use .encode('utf-8')
    115     key = hashlib.scrypt(
    116         password.encode('utf-8'),
    117         salt=salt,
    118         n=N_COST,
    119         r=R_BLOCK_SIZE,
    120         p=P_PARALLELIZATION,
    121         dklen=KEY_LENGTH
    122     ) 
    123     # 3. Store the salt AND the key (hash) together
    124     return salt + key
    125 
    126 def verify_password(stored_hash_with_salt: bytes, provided_password: str) -> bool:
    127     # 1. Separate the salt and the stored key
    128     salt = stored_hash_with_salt[:16] # Assuming 16 bytes for the salt
    129     stored_key = stored_hash_with_salt[16:]
    130     # 2. Re-derive the key from the provided password using the stored salt and parameters
    131     # **Crucially, use the exact same n, r, p, and dklen parameters!**
    132     try:
    133         derived_key = hashlib.scrypt(
    134             provided_password.encode('utf-8'),
    135             salt=salt,
    136             n=N_COST,
    137             r=R_BLOCK_SIZE,
    138             p=P_PARALLELIZATION,
    139             dklen=KEY_LENGTH
    140         )
    141         # 3. Compare the newly derived key with the stored key
    142         # Use a constant-time comparison (like `hmac.compare_digest` if available,
    143         # but Python's standard byte comparison is often okay here too)
    144         return derived_key == stored_key
    145     except ValueError:
    146         # This can happen if parameters like dklen are wrong during verification
    147         return False
    148 
    149 def logged():
    150     if session.login > 0:
    151         return True
    152     else:
    153         return False
    154 
    155 class createpost:
    156     def __init__(self, name, value):
    157         self.name = name
    158         self.value = value
    159 
    160 def savetext(thename, description):
    161     with open(basedir+thename, "w") as f:
    162         f.write(description)
    163 
    164 def savetofile(thename, description):
    165     with open(basedirthename, "w") as f:
    166         f.write(description)
    167 
    168 def loadtext(thename):
    169     if os.path.exists(basedir+thename):
    170         with open(basedir+thename, 'r') as f:
    171             description = f.read()
    172         return description
    173     return ''
    174 
    175 def savejson(name, thedict):
    176     #full path to filename
    177     #save to next line make an ID automatically, you have to check the save to know how to load it.
    178     #loadfile check if record exist, first in line is the record hash and update if it exists
    179     #make users in a folder as files first is username username will always be user record
    180     #hearts will be files as usernames with timestamp in a hearts folder in post folder. be stored and checked in u/hearts and post/hearts
    181     # be sure to add home domain setting to username
    182     #make save list a dict use same names
    183     print(thedict)
    184     print('this to be saved')
    185     thename=basedir+name
    186     if os.path.exists(thename):
    187         with open(thename, 'r') as f:
    188             p=json.load(f)
    189         print(p)
    190         print('this to be saved to')
    191         p.update(thedict)
    192         with open(thename, "w") as f:
    193             json.dump(p,f)
    194     else:
    195         with open(thename, "w") as f:
    196             #f.write(str(i) + ',')
    197             print('saving json')
    198             json.dump(thedict,f)
    199 
    200 def loadjson(name):
    201     settings = ''
    202     thename=basedir+name
    203     if os.path.exists(thename):
    204         with open(thename, 'r') as f:
    205             settings = json.load(f)
    206             #for key, i in settings.items():
    207             #    createpost(key,i)
    208     return settings
    209 
    210 def deletepost(thefile):
    211     os.system('rm '+thefile)
    212 
    213 def adduser(name, password, mail):
    214     originalname=name
    215     name=safe_filename(name[:12])
    216     password_hashed=hash_password(password).hex()
    217     tot = len(os.listdir(basedir+'r/users/'))
    218     #print(password_hashed)
    219     print('users alltsomallt: ' + str(tot))
    220     if tot > 1:
    221         adminlevel=3
    222     else:
    223         adminlevel=5
    224     thedict={'name':name, 'displayname':originalname, 'password':password_hashed,'mail':mail,'adminlevel':adminlevel}
    225     savejson('r/users/'+name, thedict)
    226     #savetext('r/user/'+name,password_hashed)
    227     print("new user added")
    228     return
    229 
    230 def updateuser(displayname, password, mail):
    231     password_hashed=hash_password(password).hex()
    232     tot = len(os.listdir(basedir+'r/users/'))
    233     thedict={'displayname':displayname, 'password':password_hashed,'mail':mail}
    234     savejson('r/users/'+session.user, thedict)
    235     print("user info updated")
    236     return
    237 
    238 def safe_filename(name: str, max_length: int = 100, replacement: str = "-") -> str:
    239     """
    240     Convert a filename into a web-safe version.
    241     """
    242     if not name:
    243         return "file"
    244     #Normalize unicode (é → e, etc.)
    245     name = unicodedata.normalize('NFKD', name)
    246     name = name.encode('ascii', 'ignore').decode('ascii')
    247     #Replace spaces and common separators with the replacement char
    248     name = re.sub(r'[\s_]+', replacement, name)
    249     #Remove any character that is not alphanumeric, hyphen, underscore, or dot
    250     name = re.sub(r'[^a-zA-Z0-9.\-_]', '', name)
    251     #Replace multiple replacement chars with single one
    252     name = re.sub(re.escape(replacement) + r'+', replacement, name)
    253     #Remove leading/trailing replacement chars and dots
    254     name = name.strip(replacement + '.')
    255     #Prevent empty or hidden files
    256     if not name or name.startswith('.'):
    257         name = "file" + name
    258     #Enforce max length (leave room for extension)
    259     if len(name) > max_length:
    260         name = name[:max_length]
    261     return name.lower()
    262 
    263 def adminlevel(user):
    264     level=loadjson('r/users/'+user)
    265     #1 session logout, web.py bug
    266     #2 rights to see pics and comment
    267     #3 rights to upoload
    268     #5 superadmin
    269     session.login = int(level['adminlevel'])
    270     return
    271 
    272 def stopresetpass(mail):
    273     t = None
    274     if os.path.exists(basedir+'r/stopresetpass/'+mail) == True:
    275         t=loadjson('r/stopresetpass/'+mail)
    276     else:
    277         thedict={'timeadded':time.time()}
    278         savejson('r/stopresetpass/'+mail, thedict)
    279         return 
    280     thedict={'timeadded':time.time()}
    281     savejson('r/stopresetpass/'+mail, thedict)
    282     latest = time.time() - t['timeadded']
    283     print(latest)
    284     if latest < 600:
    285         print('mail is in password reset spam filter')
    286         return True
    287     else:
    288         return False
    289 
    290 def stopflood(ip,referer):
    291     t = 0
    292     if os.path.exists(basedir+'r/stopflood/'+ip) == True:
    293         t=loadjson('r/stopflood/'+ip)
    294     else:
    295         thedict={'timeadded':time.time()}
    296         savejson('r/stopflood/'+ip, thedict)
    297         return
    298     thedict={'timeadded':time.time()}
    299     savejson('r/stopflood/'+ip, thedict)
    300     try:
    301         latest = time.time() - t
    302         print(latest)
    303     except:
    304         latest = 10
    305         pass
    306     if latest < 1:
    307         print('flooding recognized!')
    308         return True
    309     else:
    310         return False
    311 
    312 def getinvitation(secretinvitation):
    313     os.makedirs(basedir+'r/invites/'+session.user+'/',exist_ok=True)
    314     invite=loadjson('r/invites/'+session.user+'/'+secretinvitation)
    315     print(invite)
    316     if invite['secretinvitekey'] == secretinvitation:
    317         return True
    318     return False
    319 
    320 class trust():
    321     form = web.form.Form(
    322     web.form.Textbox('servername', web.form.notnull, description="server:"),
    323     web.form.Textbox('port', description="port:"),
    324     web.form.Textbox('user', web.form.notnull, description="user:"),
    325     web.form.Password('password', web.form.notnull, description="passcode:"),
    326     web.form.Button('Trust'))
    327     def GET(self):
    328         os.makedirs(basedir+'r/trusted/'+session.user+'/',exist_ok=True)
    329         i = web.input(remove=None)
    330         if i.remove != None:
    331             os.system('rm '+basedir+'r/trusted/'+session.user+'/'+i.remove)
    332             return web.seeother('/trust')
    333         trusted=os.listdir(basedir+'r/trusted/'+session.user+'/')
    334         trustedlist=[]
    335         for t in trusted:
    336             trusted=loadjson('r/trusted/'+session.user+'/'+t)
    337             trustedlist.append(trusted)
    338         trustform = self.form()
    339         return render.trust(trustform, trustedlist)
    340     def POST(self):
    341         i = web.input(port='')
    342         referer = web.ctx.env.get('HTTP_REFERER',baseurl)
    343         ip = web.ctx['ip']
    344         stopflood(ip, referer)
    345         loginform = self.form()
    346         i = web.input()
    347         #password_hashed=hash_password(i.password).hex()
    348         thedict={'servername':i.servername, 'port':i.port, 'user':i.user, 'password':i.password}
    349         savejson('r/trusted/'+session.user+'/'+i.servername,thedict)
    350         return web.seeother('/trust')
    351 
    352 class login():
    353     form = web.form.Form(
    354     web.form.Textbox('user', web.form.notnull, description="your registered mail account:"),
    355     web.form.Password('password', web.form.notnull, description="and your passcode please:"),
    356     web.form.Button('Login'))
    357     def GET(self):
    358         users=os.listdir(basedir+'r/users/')
    359         if len(users) == 0:
    360             result = subprocess.run(['whoami'], capture_output=True, text=True)
    361             adduser('op', 'blessyou', result.stdout.rstrip()+'@localhost')
    362         visitorlog()
    363         fejl = ''
    364         resetpasslink = False
    365         i = web.input(error=None)
    366         if i.error == 'fejl':
    367             fejl = 'wrong passcode!'
    368             resetpasslink = True
    369         if i.error == 'tom':
    370             fejl = 'didnt work'
    371         if session.login < 3:
    372             loginform = self.form()
    373             return render.login(loginform, fejl, resetpasslink)
    374         if session.login == 3:
    375             return web.seeother('/')
    376         if session.login == 5:
    377             raise web.seeother('/')
    378     def POST(self):
    379         visitorlog()
    380         referer = web.ctx.env.get('HTTP_REFERER',baseurl)
    381         ip = web.ctx['ip']
    382         stopflood(ip, referer)
    383         loginform = self.form()
    384         i = web.input()
    385         if i.user == '' or i.password == '':
    386             raise web.seeother('/login?error=tom')
    387         rymdadmins = []
    388         users = os.listdir(basedir+'r/users/')
    389         for r in users:
    390             admin=loadjson('r/users/'+r)
    391             rymdadmins.append(admin)
    392         #if not rymdadmins:
    393         #    raise web.seeother('/register')
    394         for p in rymdadmins:
    395             if p['name'].lower() == i['user'].lower() or p['mail'].lower() == i['user'].lower():
    396                 passcode=bytes.fromhex(p['password'])
    397                 if verify_password(passcode,i.password):
    398                     session.user = p['name']
    399                     adminlevel(p['name'])
    400                     if session.login == 5:
    401                         print('ACCESS!')
    402                         raise web.seeother('/')
    403                     if session.backurl != '':
    404                         backurl = session.backurl
    405                         session.backurl = ''
    406                         raise web.seeother(backurl)
    407                     else:
    408                         raise web.seeother('/')
    409         return web.seeother('/login?error=fejl')
    410 
    411 class register():
    412     form = web.form.Form(
    413     web.form.Textbox('invite', description="invitation code (do not edit):"),
    414     web.form.Textbox('user', description="name:"),
    415     web.form.Password('password', description="passcode:"),
    416     web.form.Textbox('mail', description="mail:"),
    417     web.form.Button('JOIN'))
    418     def GET(self):
    419         visitorlog()
    420         registerform = self.form()
    421         w = web.input(invite=None)
    422         formfail = ''
    423         n = ''
    424         m = ''
    425         if getinvitation(w.invite):
    426             try:
    427                 if w.fail == 'namn':
    428                     formfail = 'hey, need a name. If ya don lik ya real name imagination buddy'
    429                 if w.namn:
    430                     n = w.namn
    431                 if w.epost:
    432                     m = w.epost
    433                 elif w.epost == '':
    434                     formfail = 'we need an email, if u loose your passcode for example...'
    435                 if w.fail == 'notmail':
    436                     formfail = 'uhm, this is not an email'
    437                 elif w.fail == 'nametaken':
    438                     formfail = 'Name already taken'
    439                 elif w.fail == 'mailtaken':
    440                     formfail = 'You already got a account on this email. Try reset your passcode'
    441                 elif w.fail == 'kortlosen':
    442                     formfail = 'Too shoort passcode. Min 5 char.'
    443             except:
    444                 pass
    445             totusers = len(os.listdir(basedir+'r/users/'))
    446             registerform.fill(user=urllib.parse.unquote_plus(n), mail=urllib.parse.unquote_plus(m), invite=w.invite)
    447             return render.register(registerform, formfail, totusers)
    448         else:
    449             return web.seeother('/oopsie')
    450     def POST(self):
    451         visitorlog()
    452         registerform = self.form()
    453         i = web.input(invite=None)
    454         if getinvitation(i.invite):
    455             r = '&namn=' + i.user + '&epost=' + i.mail
    456             urllib.parse.quote_plus(r)
    457             if i.user == '':
    458                 raise web.seeother('/register?invite='+i.invite+'&fail=namn'+r)
    459             if '@' not in i.mail:
    460                 raise web.seeother('/register?invite='+i.invite+'&fail=notmail'+r)
    461             if len(i.password) < 5:
    462                 raise web.seeother('/register?invite='+i.invite+'&fail=kortlosen'+r)
    463             rymdadmins = []
    464             users = os.listdir(basedir+'r/users/')
    465             for r in users:
    466                 admin=loadjson('r/users/'+r)
    467                 rymdadmins.append(admin)
    468             for p in rymdadmins:
    469                 if p['name'].lower() == i.user.lower():
    470                     raise web.seeother('/register?invite='+i.invite+'&fail=nametaken' +r)
    471                 if p['mail'].lower() == i.mail.lower():
    472                     raise web.seeother('/register?invite='+i.invite+'&fail=mailtaken' +r)
    473             adduser(i.user, i.password, i.mail.lower())
    474             #Send mail (change messages in settings.py)
    475             sendmail(postadmin, postadmin_msg_new, postadmin_msg_new_long+i.user+' '+i.mail)
    476             #Send mail to new user
    477             sendmail(i.mail, welcome, welcome_long+i.user)
    478             #session.login = 3
    479             #session.user = safe_filename(i.user)
    480             #add user to matrix
    481             #os.system("register_new_matrix_user -u "+i.user+" -p "+i.password+" --no-admin -c /etc/matrix-synapse/homeserver.yaml") 
    482             return web.seeother('/login')
    483         else:
    484             raise web.seeother('/oopsie')
    485 
    486 class welcome():
    487     def GET(self):
    488         if session.login > 2:
    489             backurl = ''
    490             if session.backurl != '':
    491                 backurl = session.backurl
    492                 session.backurl = ''
    493             return render.ny(session.user, backurl)
    494 
    495 class like:
    496     def POST(self):
    497         if session.user != '':
    498             i = web.input(unlike=None, like=None, hate=None, unhate=None, user=None, imghash=None)
    499             user = i.user
    500             postid = i.imghash
    501             mail=''
    502             displayname=''
    503             rymdadmins=[]
    504             users = os.listdir(basedir+'r/users/')
    505             for r in users:
    506                 admin=loadjson('r/users/'+r)
    507                 rymdadmins.append(admin)
    508             for r in rymdadmins:
    509                 if r['name']==session.user:
    510                     if 'displayname' in r:
    511                         displayname=r['displayname']
    512                     if 'mail' in r:
    513                         mail=r['mail']
    514             os.makedirs(basedir+'p/posts/'+postid+'/hearts/',exist_ok=True)
    515             os.makedirs(basedir+'p/heartrank/',exist_ok=True)
    516             os.makedirs(basedir+'u/'+session.user+'/posts/'+postid+'/hearts/',exist_ok=True)
    517             try:
    518                 l = loadjson('p/posts/'+postid+'/hearts/'+session.user)
    519             except:
    520                 l={}
    521             print(session.user)
    522             if l:
    523                 user_likes = True
    524             else:
    525                 user_likes = False
    526             if user_likes == False:
    527                 try:
    528                     l=len(os.listdir(basedir+'p/posts/'+postid+'/hearts/'))
    529                 except:
    530                     l=0
    531                 l=l+1
    532                 thedict={'hearts':l}
    533                 savejson('p/posts/'+postid+'/meta',thedict)
    534                 os.system('rm '+basedir+'p/heartrank/'+postid+'-'+str(int(l-1)).zfill(16))
    535                 os.system('cp '+basedir+'p/posts/'+postid+'/meta '+basedir+'p/heartrank/'+postid+'-'+str(int(l)).zfill(16))
    536                 thedict={'hearts':l,'name':session.user,'mail':mail,'displayname':displayname,'timeadded':formattime()}
    537                 savejson('p/posts/'+postid+'/hearts/'+session.user, thedict)
    538                 savejson('u/'+session.user+'/posts/'+postid+'/hearts/'+session.user, thedict)
    539                 user_likes = True
    540             elif user_likes == True:
    541                 try:
    542                     l=len(os.listdir(basedir+'p/posts/'+postid+'/hearts/'))
    543                 except:
    544                     l=0
    545                 if l > 0:
    546                     l=l-1
    547                     thedict={'hearts':l}
    548                     savejson('p/posts/'+postid+'/meta',thedict)
    549                     deletepost(basedir+'u/'+session.user+'/posts/'+postid+'/hearts/'+session.user)
    550                     deletepost(basedir+'p/posts/'+postid+'/hearts/'+session.user)
    551                     os.system('rm '+basedir+'p/heartrank/'+postid+'-'+str(int(l+1)).zfill(16))
    552                     os.system('cp '+basedir+'p/posts/'+postid+'/meta '+basedir+'p/heartrank/'+postid+'-'+str(int(l)).zfill(16))
    553                 user_likes = False
    554             likes = len(os.listdir(basedir+'p/posts/'+postid+'/hearts/'))
    555             # Example: Update like count in your database
    556             # This is a placeholder; replace with your database logic
    557             # Return JSON response
    558             web.header('Content-Type', 'application/json')
    559             return json.dumps({'likes': likes, 'user_likes': user_likes ,'heart':heart,'hearted':hearted})
    560 
    561 class user():
    562     def GET(self, user):
    563         data = web.input(soundname=None, onair=None, public=None, showuploads=None)
    564         if user == session.user:
    565             if data.public and data.soundname:
    566                 public=data.public
    567                 thedict={'timeadded':formattime()}
    568                 savejson('p/posts/'+postid+'/meta')
    569             elif data.showuploads=='yes':
    570                 uploads = []
    571                 uploads = get_files_by_time('public_html/u/' + user + '/images/web/',newest_first=True)
    572                 return render.showuploads(uploads,user,allowedchar, random)
    573             elif data.onair and data.soundname:
    574                 onair={"onair":data.onair}
    575                 savejson('p/posts/'+postid+'/meta')
    576             #soundname='aurora_ruderalis-greatful_bread'
    577             #filetype='flac'
    578             #postid = hashlib.md5(str(random.getrandbits(256)).encode('utf-8')).hexdigest()
    579             usersounds=loadjson('r/users/'+user)
    580             #sounds=os.listdir(basedir+'p/posts/')
    581             creditsounds = []
    582             #for i in sounds:
    583             #    creditsounds.append(loadjson('p/posts/'+i))
    584             #for i in creditsound:
    585             #    try:
    586             #        credits=i['musicians'].split(',')
    587             #    except:
    588             #        credits=''
    589             #    for u in credits:
    590             #        if u.strip().lower() == user.strip().lower():
    591             #            creditsounds.append(i.title)
    592             return render.user(usersounds,creditsounds,user,datetime,str,int)
    593         return web.seeother('/login')
    594 
    595 class invites():
    596     form = web.form.Form(
    597     web.form.Textbox('mail', description="epost:"), 
    598     web.form.Button('Skicka'))
    599     def GET(self):
    600         if session.login > 2:
    601             os.makedirs(basedir+'r/invites/'+session.user+'/',exist_ok=True)
    602             invites=[]
    603             v = get_files_by_time('r/invites/'+session.user,newest_first=True) 
    604             if v:
    605                 for i in v:
    606                     invite=loadjson('r/invites/'+session.user+'/'+i)
    607                     invites.append(invite)
    608             tuningform = self.form()
    609             w = web.input(epost=None, render=None)
    610             formfail = ''
    611             if w.epost == '':
    612                 formfail = formfail + 'you have to put your email in'
    613             if w.render == 'yes':
    614                 secretinvitekey = hashlib.md5(str(random.getrandbits(256)).encode('utf-8')).hexdigest()
    615                 thedict={"secretinvitekey":secretinvitekey,"timeadded":formattime(),"creator":session.user, "accepted":''}
    616                 savejson('r/invites/'+session.user+'/'+secretinvitekey, thedict)
    617                 raise web.seeother('/invites')
    618             return render.invites(tuningform, formfail, session.user, invites)
    619     def POST(self):
    620         if session.login > 2:
    621             user = loadjson('r/users/'+session.user)
    622             tuningform = self.form()
    623             i = web.input()
    624             if i.mail == '':
    625                 raise web.seeother('/invites?fail=nomail')
    626             if '@' not in i.mail:
    627                 raise web.seeother('/tuning?fail=notmail') 
    628             secretinvitekey = hashlib.md5(str(random.getrandbits(256)).encode('utf-8')).hexdigest()
    629             thedict={"secretinvitekey":secretinvitekey,"timeadded":formattime(),"creator":session.user, "accepted":''}
    630             savejson('r/invites/'+session.user+'/'+secretinvitekey, thedict)
    631             msg = "YO! You are the One! " + user.name + " is your Morpheous. Follow this rabbit https://robinbackman.com/register?invite="+secretinvitekey 
    632             sendmail(i.mail, 'Invitation to HEART RANKED!', msg)
    633         return web.seeother('/')
    634 
    635 class tuning():
    636     form = web.form.Form(
    637     web.form.Textbox('user', description="synligt namn:"),
    638     web.form.Password('password', description="lösenord:"),
    639     web.form.Password('newpassword', description="nytt lösen (ifall du vill byta):"),
    640     web.form.Password('newpassword2', description="nytt lösen igen:"),
    641     web.form.Textbox('mail', description="epost:"), 
    642     web.form.Button('Spara'))
    643     def GET(self):
    644         if session.login > 2:
    645             user = loadjson('r/users/'+session.user)
    646             tuningform = self.form()
    647             w = web.input(namn=None,epost=None,fail=None,upd=None)
    648             formfail = ''
    649             if w.fail == 'wrongpass':
    650                 formfail = formfail + 'wrong passcode'
    651             if w.fail == 'nopass':
    652                 formfail = formfail + 'you have to write passcode'
    653             if w.namn == '':
    654                 formfail = formfail + 'whats your displayname'
    655             if w.epost == '':
    656                 formfail = formfail + 'write your email please'
    657             elif w.fail == 'notmail':
    658                 formfail = formfail + 'email please'
    659             if w.fail == 'nametaken':
    660                 formfail = formfail + 'name is taken! choose another'
    661             if w.fail == 'mailtaken':
    662                 formfail = formfail + 'mail address taken'
    663             if w.fail == 'kortlosen':
    664                 formfail = formfail + '5 characters minimum'
    665             if w.fail == 'newpass':
    666                 formfail = formfail + 'new passcode doesnt match'
    667             if w.upd == 'yes':
    668                 formfail = 'Yes, your account has been tuned in thanks!'
    669             tuningform.fill(user=user['displayname'], mail=user['mail'])
    670             return render.tuning(tuningform, formfail, user['name'])
    671         else:
    672             return web.seeother('/register')
    673     def POST(self):
    674         if session.login > 2:
    675             tuningform = self.form()
    676             i = web.input()
    677             if i.password == '':
    678                 raise web.seeother('/tuning?fail=nopass')
    679             rymdadmins = []
    680             users = os.listdir(basedir+'r/users/')
    681             for r in users:
    682                 admin=loadjson('r/users/'+r)
    683                 rymdadmins.append(admin)
    684             for p in rymdadmins:
    685                 print(p)
    686                 if p['name'] == session.user:
    687                     passcode=bytes.fromhex(p['password'])
    688                     if verify_password(passcode,i.password):
    689                         #check if display name taken
    690                         for a in rymdadmins:
    691                             if i.user in a['displayname'] and a['name'] != session.user:
    692                                 raise web.seeother('/tuning?fail=nametaken')
    693                             if i.mail in a['mail'] and i.mail != p['mail']:
    694                                 raise web.seeother('/tuning?fail=mailtaken')
    695                         if i.newpassword != '':
    696                             if i.newpassword != i.newpassword2:
    697                                 raise web.seeother('/tuning?fail=newpass')
    698                             if len(i.newpassword) < 5:
    699                                 raise web.seeother('/tuning?fail=kortlosen')
    700                             else:
    701                                 #update with password change
    702                                 updateuser(i.user,i.newpassword,i.mail)
    703                                 return web.seeother('/tuning?upd=yes')
    704                         if '@' not in i.mail:
    705                             raise web.seeother('/tuning?fail=notmail')
    706                         #update without passwordchange
    707                         thedict={'displayname':i.user,'mail':i.mail.lower()}
    708                         savejson('r/users/'+session.user, thedict)
    709                         return web.seeother('/tuning?upd=yes')
    710                     else:
    711                         raise web.seeother('/tuning?fail=wrongpass')
    712 
    713 class forgotpass():
    714     form = web.form.Form(
    715             web.form.Textbox('mail', web.form.notnull, description="email:"),
    716             web.form.Button('Send me new passcode'))
    717     def GET(self):
    718         fejl = ''
    719         i = web.input(error=None)
    720         if i.error == 'fejl':
    721             fejl = 'no email like that sorry!'
    722         elif i.error == 'done':
    723             fejl = 'your passcode is updated and sent to your mail'
    724         elif i.error == 'nej':
    725             fejl = 'nope, dont worky'
    726         elif i.error == 'stopresetpass':
    727             fejl = 'Already sent passcode to mail'
    728         if session.login < 3:
    729             loginform = self.form()
    730             return render.forgotpass(loginform, fejl)
    731         if session.login == 3:
    732             return web.seeother('/s')
    733         if session.login == 5:
    734             raise web.seeother('/s')
    735     def POST(self):
    736         referer = web.ctx.env.get('HTTP_REFERER',baseurl)
    737         ip = web.ctx['ip']
    738         stopflood(ip, referer)
    739         sendpassform = self.form()
    740         if not sendpassform.validates():
    741             raise web.seeother('/forgotpass?error=fejl')
    742         else:
    743             i = web.input()
    744             if '@' not in i.mail:
    745                 raise web.seeother('/forgotpass?error=fejl')
    746             rymdadmins = []
    747             users = os.listdir(basedir+'r/users/')
    748             for r in users:
    749                 admin=loadjson('r/users/'+r)
    750                 rymdadmins.append(admin)
    751             for p in rymdadmins:
    752                 if p['mail'].lower() == i.mail.lower():
    753                     passfilter = stopresetpass(i.mail.lower())
    754                     if passfilter == True:
    755                         raise web.seeother('/forgotpass?error=stopresetpass')
    756                     unencrypted_password = ('%06x' % random.randrange(16**6))
    757                     password = unencrypted_password.encode("utf-8")
    758                     password_hashed=hash_password(password).hex()
    759                     thedict={'password':password_hashed}
    760                     savejson('r/users/'+session.user, thedict)
    761                     print("lösenordet uppdaterat!")
    762                     msg = "Your new passcode is: " + unencrypted_password
    763                     sendmail(p.mail, 'Heart Ranked Passcode', msg)
    764                     raise web.seeother('/forgotpass?error=done')
    765             raise web.seeother('/forgotpass?error=fejl')
    766 
    767 def sendmail(email, subject, msg):
    768     #Send mail
    769     try:
    770         echomsg = subprocess.Popen(('echo', msg+'\n'+postadmin_signature), stdout=subprocess.PIPE)
    771         sendmsg = subprocess.check_output(('mail', '-r', postadmin, '-s', subject, email), stdin=echomsg.stdout)
    772         echomsg.wait()
    773     except:
    774         print('no mail server found')
    775         #subprocess.call(['echo', msg, '|', 'mail', '-r', postadmin,'-s', subject, email])
    776 
    777 def get_new_frames(gif, scale):
    778     new_frames = []
    779     actual_frames = gif.n_frames
    780     for frame in range(actual_frames):
    781         gif.seek(frame)
    782         new_frame = Image.new('RGBA', gif.size)
    783         new_frame.paste(gif)
    784         new_frame.thumbnail(scale, Image.Resampling.LANCZOS)
    785         new_frames.append(new_frame)
    786     return new_frames
    787 
    788 def save_new_gif(new_frames, old_gif_information, new_path):
    789     new_frames[0].save(new_path,
    790                        save_all = True,
    791                        append_images = new_frames[1:],
    792                        duration = old_gif_information['duration'],
    793                        loop = old_gif_information['loop'],
    794                        background = old_gif_information['background'],
    795                        extension = old_gif_information['extension'] ,
    796                        transparency = old_gif_information['transparency'])
    797 
    798 def resize_gif(input_path, output_path, max_size):
    799     input_image = Image.open(input_path)
    800     frames = list(_thumbnail_frames(input_image,max_size))
    801     output_image = frames[0]
    802     output_image.save(
    803         output_path,
    804         save_all=True,
    805         append_images=frames[1:],
    806         disposal=input_image.disposal_method,
    807         **input_image.info,
    808     )
    809 
    810 def _thumbnail_frames(image,max_size):
    811     for frame in ImageSequence.Iterator(image):
    812         new_frame = frame.copy()
    813         new_frame.thumbnail(max_size, Image.Resampling.LANCZOS)
    814         yield new_frame
    815 
    816 def scale_gif(path, scale, new_path=None):
    817     gif = Image.open(path)
    818     if not new_path:
    819         new_path = path
    820     old_gif_information = {
    821         'loop': bool(gif.info.get('loop', 1)),
    822         'duration': gif.info.get('duration', 40),
    823         'background': gif.info.get('background', 223),
    824         'extension': gif.info.get('extension', (b'NETSCAPE2.0')),
    825         'transparency': gif.info.get('transparency', 223)
    826     }
    827     new_frames = get_new_frames(gif, scale)
    828     save_new_gif(new_frames, old_gif_information, new_path)
    829 
    830 
    831 def getdisplayname(user):
    832     try:
    833         displayname = loadjson('r/users/'+session.user)
    834         displayname = displayname.displayname
    835     except:
    836         displayname = user
    837     return displayname
    838 
    839 #-------------Get files and sort em by date modified---------------
    840 
    841 def get_dirs_by_time(path, reverse=False):
    842     """Return names of immediate subdirectories, sorted by modification time."""
    843     path=basedir+path
    844     with os.scandir(path) as it:
    845         return sorted(
    846             (e.name for e in it if e.is_dir(follow_symlinks=False)),
    847             key=lambda name: os.stat(os.path.join(path, name)).st_mtime,
    848             reverse=reverse,
    849         )
    850 
    851 def get_posts_by_time(path,reverse):
    852     posts = os.listdir(path)
    853     timebased=[]
    854     for p in posts:
    855         l=loadjson('p/posts/'+p+'/meta')
    856         timebased.append((l['postid'],l['timeadded']))
    857     fmt = datetimeformat
    858     sorted_posts = sorted(timebased, key=lambda x: datetime.datetime.strptime(x[1], fmt),reverse=reverse)
    859     return sorted_posts
    860 
    861 def get_files_by_time(directory: str = ".", newest_first: bool = True):
    862     path = Path(basedir+directory) 
    863     #Get all files (exclude directories and hidden files if you want)
    864     files = [f for f in path.iterdir() if f.is_file()]
    865     #Sort by modification time
    866     sorted_files = sorted(
    867         files,
    868         key=lambda f: f.stat().st_mtime,   # last modified timestamp
    869         reverse=newest_first
    870     ) 
    871     # Return just the file names (as strings)
    872     return [f.name for f in sorted_files]
    873 
    874 def getfiles(filmfolder):
    875     #get a list of films, in order of settings.p file last modified
    876     films_sorted = []
    877     films = next(os.walk(filmfolder))[1]
    878     for i in films:
    879         uploaded = os.listdir(filmfolder + i + '/')
    880         for f in uploaded:
    881             if os.path.isfile(filmfolder + i + '/'+f) == True:
    882                 #DUUUUUUDE HERE CHECK IF OLDER THAN
    883                 lastupdate = os.path.getmtime(filmfolder + i + '/' + f)
    884                 films_sorted.append((i,f,lastupdate))
    885         else:
    886             films_sorted.append((i,f,0))
    887     films_sorted = sorted(films_sorted, key=lambda tup: tup[2], reverse=True)
    888     return films_sorted
    889 
    890 def callsubprocess(cmd):
    891     subprocess.call(cmd.split())
    892 
    893 def visitorlog():
    894     ip = web.ctx['ip']
    895     referer = web.ctx.env.get('HTTP_REFERER', 'none')
    896     environ = web.ctx.env.get('HTTP_USER_AGENT', 'dunno')
    897     last = get_files_by_time('r/visitors/',newest_first=True)
    898     stopflood(ip, referer)
    899     country='local loco'
    900     countrycode='fi'
    901     if last:
    902         lastip=loadjson('r/visitors/'+last[0])
    903     else:
    904         lastip=''
    905         try:
    906             country = ''
    907             country = os.popen('geoiplookup '+ip).read()
    908             print(country)
    909             if country != '':
    910                 countrycode = country.split(':')[1].split(',')[0].lower().strip()
    911                 country = country.split(':')[1].split(',')[1].strip()
    912             else:
    913                 country='none'
    914         except:
    915             country='local loco'
    916             countrycode='fi'
    917         thedict={'ip':ip,'referer':referer,'environ':environ,'country':country,'countrycode':countrycode,'time':formattime()}
    918         savejson('r/visitors/'+ip,thedict)
    919         print("added to visitor log")
    920     if lastip != ip:
    921         try:
    922             country = ''
    923             country = os.popen('geoiplookup '+ip).read()
    924             #print(soundtype)
    925             countrycode = country.split(':')[1].split(',')[0].lower().strip()
    926             country = country.split(':')[1].split(',')[1].strip()
    927             thedict={'ip':ip,'referer':referer,'environ':environ,'country':country,'countrycode':countrycode,'time':formattime()}
    928             savejson('r/visitors/'+ip,thedict)
    929         except:
    930             pass
    931         print("added to visitor log")
    932     return
    933 
    934 def getvisitors():
    935     visitors=[]
    936     v = get_files_by_time('r/visitors/',newest_first=True) 
    937     if v:
    938         for i in v:
    939             visit=loadjson('r/visitors/'+i)
    940             visitors.append(visit)
    941             print(visit)
    942     total=len(os.listdir(basedir+'r/visitors/'))
    943     unique=[]
    944     for i in visitors:
    945         for p in visitors:
    946             if i['ip'] == p['ip']:
    947                 unique.append(i)
    948     uniquevisits=len(unique)
    949     return visitors, total, uniquevisits
    950 
    951 def getvisits():
    952     visitors=[]
    953     #v=os.listdir(basedir+'r/visitors/')
    954     v = get_files_by_time('r/visitors/',newest_first=True) 
    955     for i in v:
    956         visit=loadjson('r/visitors/'+i)
    957         visitors.append(visit)
    958     total=len(os.listdir(basedir+'r/visitors/'))
    959     unique=[]
    960     for i in visitors:
    961         for p in visitors:
    962             if i['ip'] == p['ip']:
    963                 unique.append(i)
    964     uniquevisits=len(unique)
    965     countrylist=[]
    966     for i in visitors:
    967         if i['countrycode'] not in countrylist:
    968             countrylist.append(i['countrycode'])
    969     return countrylist, total, uniquevisits
    970 
    971 class stats:
    972     def GET(self):
    973         p = web.input(logfilter=None)
    974         visitors, total, unique = getvisitors()
    975         return rendersplash.stats(visitors, total, unique, p.logfilter)
    976 
    977 class logout:
    978     def GET(self):
    979         session.login = 0
    980         session.user = None
    981         raise web.seeother('/')
    982 
    983 def getlikes(postid, user):
    984     user_likes = False
    985     os.makedirs(basedir+'p/posts/'+postid+'/hearts/',exist_ok=True)
    986     try:
    987         l=len(os.listdir(basedir+'p/posts/'+postid+'/hearts/'))
    988         print(os.listdir(basedir+'p/posts/'+postid+'/hearts/'))
    989     except:
    990         l=0
    991     if user != None:
    992         m=loadjson('/p/posts/'+postid+'/hearts/'+user)
    993         if m:
    994             user_likes = True
    995         else:
    996             user_likes = False
    997     if l >= 0:
    998         if user_likes: 
    999             likes = hearted+" " + str(l)
   1000         else: 
   1001             if l > 0:
   1002                 likes = heart+" " + str(l)
   1003             else:
   1004                 likes = heart
   1005         return likes
   1006 
   1007 def postexist(postid):
   1008     try:
   1009         l=loadjson('/p/posts/'+postid+'/meta')
   1010     except:
   1011         return False
   1012     try:
   1013         if l['postid'] != None:
   1014             return True
   1015         else:
   1016             return False
   1017     except:
   1018         return False
   1019     return False
   1020 
   1021 def getcombines(postid):
   1022     try:
   1023         l=len(os.listdir(basedir+'p/posts/'+postid+'/combos/'))
   1024     except:
   1025         l=0
   1026     print(postid)
   1027     if int(l) > 0:
   1028         return "⚭ " + str(l)
   1029     else:
   1030         return ''
   1031 
   1032 def pushcombines(postid):
   1033     try:
   1034         l=len(os.listdir(basedir+'p/posts/'+postid+'/combos/'))
   1035     except:
   1036         l=0
   1037     #thedict={'combines':l}
   1038     #savejson('/p/posts/'+postid+'/meta',thedict)
   1039     if l >= 0:
   1040             return "⚭ " + str(l)
   1041     else:
   1042         return ''
   1043 
   1044 def formattime():
   1045     current_time = time.gmtime()
   1046     str_time = time.strftime(datetimeformat, current_time)
   1047     return str_time
   1048 
   1049 def sort_by_name_then_time(path):
   1050     sortedposts=[]
   1051     heartrank=os.listdir(basedir+path)
   1052     for h in heartrank:
   1053         postid=h.split('-')[0]
   1054         postrank=h.split('-')[1]
   1055         sortedposts.append((postrank,postid))
   1056     heartrank=sorted(sortedposts, reverse=True)
   1057     return heartrank
   1058 
   1059 def rankrender():
   1060     posts = os.listdir(basedir+'p/posts')
   1061     os.system('rm -r '+basedir+'p/heartrank')
   1062     os.system('rm -r '+basedir+'p/comborank')
   1063     os.makedirs(basedir+'p/comborank',exist_ok=True)
   1064     os.makedirs(basedir+'p/heartrank',exist_ok=True)
   1065     for postid in posts:
   1066         try:
   1067             l=len(os.listdir(basedir+'p/posts/'+postid+'/hearts/'))
   1068         except:
   1069             l=0
   1070         thedict={'hearts':l}
   1071         savejson('p/posts/'+postid+'/meta',thedict)
   1072         os.system('cp '+basedir+'p/posts/'+postid+'/meta '+basedir+'p/heartrank/'+postid+'-'+str(int(l)).zfill(16))
   1073         try:
   1074             l=len(os.listdir(basedir+'p/posts/'+postid+'/combos/'))
   1075         except:
   1076             l=0
   1077         thedict={'combos':l}
   1078         savejson('p/posts/'+postid+'/meta',thedict)
   1079         os.system('cp '+basedir+'p/posts/'+postid+'/meta '+basedir+'p/comborank/'+postid+'-'+str(int(l)).zfill(16))
   1080 
   1081 rankrender()
   1082 
   1083 def getfeed():
   1084     timebase=session.timebase
   1085     feedbase=session.feedbase
   1086     usr=session.usrfeed
   1087     if feedbase == '':
   1088         feedbase = 'time'
   1089     if timebase == '':
   1090         timebase = 'week'
   1091     now = datetime.datetime.now()
   1092     goodies=[]
   1093     #HEARTS
   1094     #SAVE HEARTRANKING EVERY MINUTE CHECK BOTH USER LIKES AND POST LIKES IF IT CHECKS OUT GOOD IT NOT WRITE ERROR AT LEAST (A BACKEND PROGRAM, RUNS EVERY MINUTE AND COUNTS LIKES AND WRITES THE HEARTRANKING FOR TODAY. HEARTRANKING STAYES SAVED FOREVER IN FOLDERS BY DAYS. IT IS A FOLDER WITH NUMBERS. starting with 0000000000000001 pointing to postid. simple. effective.
   1095     #backend program will also sync posts and likes to trustees
   1096     if feedbase == "heart" and timebase == "today":
   1097         one_day_before = now - datetime.timedelta(days=1)
   1098         posts = sort_by_name_then_time('p/heartrank/')
   1099         for p in posts:
   1100             #check modtime here day
   1101             p=p[1]
   1102             lastupdate = os.path.getmtime(basedir+'p/posts/'+p+'/meta')
   1103             if datetime.datetime.fromtimestamp(lastupdate) > one_day_before:
   1104                 l=loadjson('p/posts/'+p+'/meta')
   1105                 if usr=='':
   1106                     goodies.append(l)
   1107                 if usr==l['creator']:
   1108                     goodies.append(l)
   1109     elif feedbase == "heart" and timebase == "week":
   1110         one_day_before = now - datetime.timedelta(weeks=1)
   1111         posts = sort_by_name_then_time('p/heartrank/')
   1112         for p in posts:
   1113             #check modtime here day
   1114             p=p[1]
   1115             lastupdate = os.path.getmtime(basedir+'p/posts/'+p+'/meta')
   1116             if datetime.datetime.fromtimestamp(lastupdate) > one_day_before:
   1117                 l=loadjson('p/posts/'+p+'/meta')
   1118                 if usr=='':
   1119                     goodies.append(l)
   1120                 if usr==l['creator']:
   1121                     goodies.append(l)
   1122     elif feedbase == "heart" and timebase == "month":
   1123         one_day_before = now - datetime.timedelta(weeks=4)
   1124         posts = sort_by_name_then_time('p/heartrank/')
   1125         for p in posts:
   1126             #check modtime here day
   1127             p=p[1]
   1128             lastupdate = os.path.getmtime(basedir+'p/posts/'+p+'/meta')
   1129             if datetime.datetime.fromtimestamp(lastupdate) > one_day_before:
   1130                 l=loadjson('p/posts/'+p+'/meta')
   1131                 if usr=='':
   1132                     goodies.append(l)
   1133                 if usr==l['creator']:
   1134                     goodies.append(l)
   1135     elif feedbase == "heart" and timebase == "year":
   1136         one_day_before = now - datetime.timedelta(weeks=54)
   1137         posts = sort_by_name_then_time('p/heartrank/')
   1138         for p in posts:
   1139             #check modtime here day
   1140             p=p[1]
   1141             lastupdate = os.path.getmtime(basedir+'p/posts/'+p+'/meta')
   1142             if datetime.datetime.fromtimestamp(lastupdate) > one_day_before:
   1143                 l=loadjson('p/posts/'+p+'/meta')
   1144                 if usr=='':
   1145                     goodies.append(l)
   1146                 if usr==l['creator']:
   1147                     goodies.append(l)
   1148     elif feedbase == "heart" and timebase == "" or feedbase == "heart" and timebase == "all":
   1149         posts = sort_by_name_then_time('/p/heartrank/')
   1150         for p in posts:
   1151             p=p[1]
   1152             l=loadjson('p/posts/'+p+'/meta')
   1153             if usr=='':
   1154                 goodies.append(l)
   1155             if usr==l['creator']:
   1156                 goodies.append(l)
   1157     #TIME
   1158     elif feedbase == "time" and timebase == "today":
   1159         one_day_before = now - datetime.timedelta(days=1)
   1160         posts = get_posts_by_time(basedir+'p/posts/',reverse=True)
   1161         print('fuuuuuuuuuu')
   1162         print(posts)
   1163         for p in posts:
   1164             p=p[0]
   1165             #check modtime here day
   1166             lastupdate = os.path.getmtime(basedir+'p/posts/'+p+'/meta')
   1167             if datetime.datetime.fromtimestamp(lastupdate) > one_day_before:
   1168                 l=loadjson('p/posts/'+p+'/meta')
   1169                 if usr=='':
   1170                     goodies.append(l)
   1171                 if usr==l['creator']:
   1172                     goodies.append(l)
   1173     elif feedbase == "time" and timebase == "week":
   1174         one_day_before = now - datetime.timedelta(weeks=1)
   1175         posts = get_posts_by_time(basedir+'p/posts/',reverse=True)
   1176         for p in posts:
   1177             p=p[0]
   1178             #check modtime here day
   1179             lastupdate = os.path.getmtime(basedir+'p/posts/'+p+'/meta')
   1180             if datetime.datetime.fromtimestamp(lastupdate) > one_day_before:
   1181                 l=loadjson('p/posts/'+p+'/meta')
   1182                 if usr=='':
   1183                     goodies.append(l)
   1184                 if usr==l['creator']:
   1185                     goodies.append(l)
   1186     elif feedbase == "time" and timebase == "month":
   1187         one_day_before = now - datetime.timedelta(weeks=4)
   1188         posts = get_posts_by_time(basedir+'p/posts/',reverse=True)
   1189         for p in posts:
   1190             p=p[0]
   1191             #check modtime here day
   1192             lastupdate = os.path.getmtime(basedir+'p/posts/'+p+'/meta')
   1193             if datetime.datetime.fromtimestamp(lastupdate) > one_day_before:
   1194                 l=loadjson('p/posts/'+p+'/meta')
   1195                 if usr=='':
   1196                     goodies.append(l)
   1197                 if usr==l['creator']:
   1198                     goodies.append(l)
   1199     elif feedbase == "time" and timebase == "year":
   1200         one_day_before = now - datetime.timedelta(weeks=54)
   1201         posts = get_posts_by_time(basedir+'p/posts/',reverse=True)
   1202         for p in posts:
   1203             p=p[0]
   1204             #check modtime here day
   1205             lastupdate = os.path.getmtime(basedir+'p/posts/'+p+'/meta')
   1206             if datetime.datetime.fromtimestamp(lastupdate) > one_day_before:
   1207                 l=loadjson('p/posts/'+p+'/meta')
   1208                 if usr=='':
   1209                     goodies.append(l)
   1210                 if usr==l['creator']:
   1211                     goodies.append(l)
   1212     elif feedbase == "time" and timebase == "" or  feedbase == "time" and timebase == "all":
   1213         posts = get_posts_by_time(basedir+'p/posts/',reverse=True)
   1214         for p in posts:
   1215             p=p[0]
   1216             l=loadjson('p/posts/'+p+'/meta')
   1217             if usr=='':
   1218                 goodies.append(l)
   1219             if usr==l['creator']:
   1220                 goodies.append(l)
   1221 
   1222     #COMBO
   1223     elif feedbase == "combo" and timebase == "today":
   1224         one_day_before = now - datetime.timedelta(days=1)
   1225         posts = sort_by_name_then_time('p/comborank/')
   1226         for p in posts:
   1227             #check modtime here day
   1228             p=p[1]
   1229             lastupdate = os.path.getmtime(basedir+'p/posts/'+p+'/meta')
   1230             if datetime.datetime.fromtimestamp(lastupdate) > one_day_before:
   1231                 l=loadjson('p/posts/'+p+'/meta')
   1232                 if usr=='':
   1233                     goodies.append(l)
   1234                 if usr==l['creator']:
   1235                     goodies.append(l)
   1236     elif feedbase == "combo" and timebase == "week":
   1237         one_day_before = now - datetime.timedelta(weeks=1)
   1238         posts = sort_by_name_then_time('p/comborank/')
   1239         for p in posts:
   1240             #check modtime here day
   1241             p=p[1]
   1242             lastupdate = os.path.getmtime(basedir+'p/posts/'+p+'/meta')
   1243             if datetime.datetime.fromtimestamp(lastupdate) > one_day_before:
   1244                 l=loadjson('p/posts/'+p+'/meta')
   1245                 if usr=='':
   1246                     goodies.append(l)
   1247                 if usr==l['creator']:
   1248                     goodies.append(l)
   1249     elif feedbase == "combo" and timebase == "month":
   1250         one_day_before = now - datetime.timedelta(weeks=4)
   1251         posts = sort_by_name_then_time('p/comborank/')
   1252         for p in posts:
   1253             #check modtime here day
   1254             p=p[1]
   1255             lastupdate = os.path.getmtime(basedir+'p/posts/'+p+'/meta')
   1256             if datetime.datetime.fromtimestamp(lastupdate) > one_day_before:
   1257                 l=loadjson('p/posts/'+p+'/meta')
   1258                 if usr=='':
   1259                     goodies.append(l)
   1260                 if usr==l['creator']:
   1261                     goodies.append(l)
   1262     elif feedbase == "combo" and timebase == "year":
   1263         one_day_before = now - datetime.timedelta(weeks=54)
   1264         posts = sort_by_name_then_time('p/comborank/')
   1265         for p in posts:
   1266             #check modtime here day
   1267             p=p[1]
   1268             lastupdate = os.path.getmtime(basedir+'p/posts/'+p+'/meta')
   1269             if datetime.datetime.fromtimestamp(lastupdate) > one_day_before:
   1270                 l=loadjson('p/posts/'+p+'/meta')
   1271                 if usr=='':
   1272                     goodies.append(l)
   1273                 if usr==l['creator']:
   1274                     goodies.append(l)
   1275     elif feedbase == "combo" and timebase == "" or  feedbase == "combo" and timebase == "all":
   1276         posts = sort_by_name_then_time('p/comborank/')
   1277         for p in posts:
   1278             #check modtime here day
   1279             p=p[1]
   1280             l=loadjson('p/posts/'+p+'/meta')
   1281             if usr=='':
   1282                 goodies.append(l)
   1283             if usr==l['creator']:
   1284                 goodies.append(l)
   1285     elif feedbase == "Idontevenknow":
   1286         print('write your own custom feed here')
   1287     else:
   1288         posts = os.listdir('p/posts/')
   1289         for p in posts:
   1290             l=loadjson('p/posts/'+p+'/meta')
   1291             if usr=='':
   1292                 goodies.append(l)
   1293             if usr==l['creator']:
   1294                 goodies.append(l)
   1295     return goodies
   1296 
   1297 def getcombofeed(show):
   1298     timebase=session.timebase
   1299     feedbase=session.feedbase
   1300     if feedbase == "heart":
   1301         posts = sort_by_name_then_time('p/heartrank/')
   1302         comboposts=[]
   1303         for p in posts: 
   1304             p=p[1]
   1305             l=loadjson('p/posts/'+p+'/meta')
   1306             if 'combine' in l:
   1307                 if l['combine'] == show:
   1308                     comboposts.append(l)
   1309     elif feedbase == "combo":
   1310         posts = sort_by_name_then_time('p/comborank/')
   1311         comboposts=[]
   1312         for p in posts:
   1313             p=p[1]
   1314             l=loadjson('p/posts/'+p+'/meta')
   1315             if 'combine' in l:
   1316                 if l['combine'] == show:
   1317                     comboposts.append(l)
   1318     else:
   1319         posts = get_dirs_by_time('p/posts/',reverse=True)
   1320         comboposts=[]
   1321         for p in posts:
   1322             l=loadjson('p/posts/'+p+'/meta')
   1323             if 'combine' in l:
   1324                 if l['combine'] == show:
   1325                     comboposts.append(l)
   1326     return comboposts
   1327 
   1328 def userimage(user):
   1329     usrimg = ''
   1330     i = staticdir+'users/'+user+'/images/thumb/'+user
   1331     print(i)
   1332     if os.path.isfile(i+'.jpeg'):
   1333         usrimg='static/users/'+user+'/images/thumb/'+user+'.jpeg'
   1334     elif os.path.isfile(i+'.jpg'):
   1335         usrimg='static/users/'+user+'/images/thumb/'+user+'.jpg'
   1336     elif os.path.isfile(i+'.png'):
   1337         usrimg='static/users/'+user+'/images/thumb/'+user+'.png'
   1338     elif os.path.isfile(i+'.gif'):
   1339         usrimg='static/users/'+user+'/images/thumb/'+user+'.gif'
   1340     if usrimg != '':
   1341         imghtml='<img class="usrimg" src="'+usrimg+'">'
   1342         return imghtml
   1343     else:
   1344         return 
   1345 
   1346 class heartranked:
   1347     form = web.form.Form(web.form.Textbox('search', web.form.notnull, description="or search"))
   1348     def GET(self):
   1349         siterendered=formattime()
   1350         visitorlog()
   1351         visitors, total, unique = getvisits()
   1352         print(visitors)
   1353         print(str(total))
   1354         print(str(unique))
   1355         searchform = self.form()
   1356         bildpersida = 1000
   1357         session.search = ''
   1358         session.bildsida = 0
   1359         i = web.input(publised=None, public=None, show=None, remove=None, edit=None, feedbase=None, timebase=None, usrfeed=None)
   1360         #search
   1361         try:
   1362             bilder_totalt=os.listdir(basedir+'p/posts/')
   1363             tot = len(bilder_totalt)
   1364             #print('bilder alltsomallt: ' + str(tot))
   1365         except:
   1366             #print("inga bilder")
   1367             tot = 0
   1368         #print('session search: ' + session.search)
   1369         try:
   1370             if i.search == '':
   1371                 session.search = ''
   1372                 session.bildsida = 0
   1373             elif i.search != "":
   1374                 session.search = urllib.parse.unquote_plus(i.search)
   1375                 session.bildsida = 0
   1376         except:
   1377             pass
   1378         if session.search != '':
   1379             search_result = []
   1380             tot = 0
   1381             try:
   1382                 for i in bilder_totalt:
   1383                     searchthis=loadjson('p/posts/'+i+'/meta')
   1384                     for p in dir(searchthis):
   1385                         if session.search in p:
   1386                             search_result.append(searchthis)
   1387                             tot=tot+1
   1388             except:
   1389                 pass
   1390             try:
   1391                 print(search_result)
   1392                 #print('sökta bilder: ' + str(tot))
   1393             except:
   1394                 pass
   1395         try:
   1396             if i.page == "next":
   1397                 if session.bildsida < tot:
   1398                     session.bildsida += bildpersida
   1399             if i.page == "back":
   1400                 if session.bildsida > bildpersida:
   1401                     session.bildsida -= bildpersida
   1402                 else:
   1403                     session.bildsida = 0
   1404         except:
   1405             pass
   1406         limit = session.bildsida + bildpersida
   1407         offset = session.bildsida
   1408         #EOF search
   1409         #print(session.bildsida)
   1410         bilder=[]
   1411         if session.search == '':
   1412             posts=os.listdir(basedir+'p/posts/')
   1413             for p in posts:
   1414                 this=loadjson('p/posts/'+p+'/meta')
   1415                 bilder.append(this)
   1416         else:
   1417             bilder = search_result
   1418         if i.feedbase == None:
   1419             feedbase = ''
   1420         else:
   1421             feedbase = i.feedbase
   1422             session.feedbase = feedbase
   1423         if i.timebase == None:
   1424             timebase = ''
   1425         else:
   1426             timebase = i.timebase
   1427             session.timebase = timebase
   1428         if i.timebase == None:
   1429             timebase = ''
   1430         else:
   1431             timebase = i.timebase
   1432             session.timebase = timebase
   1433         if i.usrfeed == None:
   1434             usrfeed = ''
   1435         else:
   1436             usrfeed = i.usrfeed
   1437             session.usrfeed = usrfeed
   1438         if session.user=='':
   1439             free_hash_for_user = hashlib.md5(str(random.getrandbits(256)).encode('utf-8')).hexdigest()[:4]
   1440             #session.user = 'heart_'+free_hash_for_user
   1441             session.user = None
   1442         if i.edit != None:
   1443             session.postid=i.edit
   1444             raise web.seeother('/editor?public=yes') 
   1445         if i.remove != None:
   1446             try:
   1447                 post=loadjson('p/posts/'+i.remove+'/meta')
   1448                 if post['creator'] == session.user:
   1449                     os.makedirs(basedir+'u/'+session.user+'/deleted/', exist_ok=True)
   1450                     os.makedirs(basedir+'p/deleted/', exist_ok=True)
   1451                     os.system('mv '+basedir+'u/'+session.user+'/posts/'+i.remove+' '+basedir+'u/'+session.user+'/deleted/')
   1452                     os.system('mv '+basedir+'p/posts/'+i.remove+' '+basedir+'p/deleted/')
   1453                     print('move to a deleted folder, make backend clean things up for real')
   1454             except:
   1455                 pass
   1456         if session.login > 3:
   1457             try:
   1458                 if i.delete != '':
   1459                     return web.seeother('/remove/' + i.delete)
   1460             except:
   1461                 pass
   1462             rights = 'admin'
   1463         elif session.login > 2:
   1464             rights = 'mod'
   1465         else:
   1466             rights = 'spacer'
   1467         if session.theme != theme:
   1468             session.theme = theme
   1469         return rendersplash.heartranked(markdown, visitors, total, unique, logged, rights, session.user, getlikes, formattime, feedbase, tot, limit, offset, bildpersida, session.search, bilder, searchform, getcombines, timebase, getfeed, getcombofeed, userimage, postexist, i.show, loadjson, loadtext, len, heart, hearted, sitename, siteslogan, siterendered, siteurl, session.usrfeed, theme)
   1470     def POST(self):
   1471         searchform = self.form()
   1472         i = web.input()
   1473         if i.search != '':
   1474             raise web.seeother('/?search='+i.search)
   1475 
   1476 def zippitandshippit(postid):
   1477     #also zippit here!
   1478     os.makedirs(basedir+'r/trusted/'+session.user+'/', exist_ok=True)
   1479     os.system('ln -s '+basedir+'p/zipped '+staticdir+'users/'+session.user+'/zipped')
   1480     os.system('cd '+basedir+'p/posts/ && zip -o -r '+postid+'.zip '+postid )
   1481     os.system('mv '+basedir+'p/posts/'+postid+'.zip '+basedir+'/p/zipped/')
   1482     #LETS SHIPPIT!
   1483     trustedlist=[]
   1484     trusted=os.listdir(basedir+'r/trusted/'+session.user+'/')
   1485     for t in trusted:
   1486         trusted=loadjson('r/trusted/'+session.user+'/'+t)
   1487         trustedlist.append(trusted)
   1488     for t in trustedlist:
   1489         url=t['servername']+':'+t['port']
   1490         for a in allowedchar:
   1491             if '.'+a in t['servername']: #is webaddress use https
   1492                 url='https://'+t['servername']
   1493         passcode=t['password']
   1494         trustedlogin = ['curl','-X','POST', url+'/login', '-i', '-b', basedir+'/sessions/sessions-'+session.user, '-c',basedir+'/sessions/sessions-'+session.user, '-d', 'user='+t['user'], '-d', 'password='+passcode]
   1495         subprocess.check_output(trustedlogin)
   1496         #OK GOT EM COOKIES LES DO IT DO IT DO IT SHIPPIT!
   1497         shippit = ['curl','-X', 'POST', '--verbose', '--header', 'Content-Type: multipart/form-data', '-F', 'files=@'+basedir+'p/zipped/'+postid+'.zip;type=application/zip', '-b', basedir+'/sessions/sessions-'+session.user, '-c',basedir+'/sessions/sessions-'+session.user, url+'/upload/ship']
   1498         subprocess.check_output(shippit)
   1499 
   1500 storage = {"content": ""}
   1501 class editor:
   1502     def GET(self):
   1503         if logged():
   1504             i = web.input(publish=None, public=None, new=None, combine=None, remix=None, saveto='')
   1505             if i.saveto != '':
   1506                 if session.login == 5:
   1507                     session.saveto=i.saveto
   1508             if i.combine != None:
   1509                 if session.user:
   1510                     text=''
   1511                     text2=''
   1512                     session.postid = hashlib.sha256(str(random.getrandbits(256)).encode('utf-8')).hexdigest()[9:36]
   1513                     os.makedirs(basedir+'u/'+session.user+'/posts/'+session.postid, exist_ok=True)
   1514                     thedict={'postid':session.postid, 'siteurl':siteurl, 'timeadded':formattime(), 'creator':session.user, 'combine':i.combine}
   1515                     savejson('u/'+session.user+'/posts/'+session.postid+'/meta',thedict)
   1516             if i.remix != None:
   1517                 if session.user:
   1518                     text=''
   1519                     text2=''
   1520                     try:
   1521                         olduser=loadjson('p/posts/'+i.remix+'/meta')
   1522                         text=loadtext('p/posts/'+i.remix+'/intro')
   1523                         text2=loadtext('p/posts/'+i.remix+'/post')
   1524                     except:
   1525                         pass
   1526                     try:
   1527                         olduser = olduser['creator']
   1528                     except:
   1529                         olduser = ''
   1530                     if olduser != '':
   1531                         allcreators = olduser+','+session.user
   1532                     else:
   1533                         allcreators = session.user
   1534                     session.postid = hashlib.sha256(str(random.getrandbits(256)).encode('utf-8')).hexdigest()[9:36]
   1535                     os.makedirs(basedir+'u/'+session.user+'/posts/'+session.postid, exist_ok=True)
   1536                     thedict={'postid':session.postid,  'siteurl':siteurl,'timeadded':formattime(), 'creator':allcreators, 'remix':i.remix}
   1537                     os.makedirs(basedir+'p/posts/'+i.remix+'/remix', exist_ok=True)
   1538                     savetext('p/posts/'+i.remix+'/remix/'+session.postid,session.user)
   1539 
   1540                     savejson('u/'+session.user+'/posts/'+session.postid+'/meta',thedict)
   1541                     savetext('u/'+session.user+'/posts/'+session.postid+'/post', text2)
   1542                     savetext('u/'+session.user+'/posts/'+session.postid+'/intro', text)
   1543             if session.postid != '':
   1544                 if i.public=='yes':
   1545                     try:
   1546                         text=loadtext('p/posts/'+session.postid+'/intro')
   1547                     except:
   1548                         session.postid = ''
   1549                 else:
   1550                     try:
   1551                         text=loadtext('u/'+session.user+'/posts/'+session.postid+'/intro')
   1552                     except:
   1553                         text = ''
   1554                 if i.public=='yes':
   1555                     try:
   1556                         text2=loadtext('p/posts/'+session.postid+'/post')
   1557                     except:
   1558                         session.postid = ''
   1559                 else:
   1560                     try:
   1561                         text2=loadtext('u/'+session.user+'/posts/'+session.postid+'/post')
   1562                     except:
   1563                         text2=''
   1564             else:
   1565                 text = ''
   1566                 text2 = ''
   1567             if i.saveto != '':
   1568                 text=i.saveto
   1569                 text2=loadtext(i.saveto)
   1570             if i.new == 'yes':
   1571                 session.postid = ''
   1572                 raise web.seeother('/editor')
   1573             if i.publish == 'yes' and text != '' and i.public == None and logged() and len(text) < 256:
   1574                 if session.saveto != '':
   1575                     session.saveto=''       
   1576                     raise web.seeother('/')
   1577                 c=loadjson('u/'+session.user+'/posts/'+session.postid+'/meta')
   1578                 if 'combine' in c:
   1579                     if c['combine'] != '':
   1580                         #calculate comborank
   1581                         os.makedirs(basedir+'p/posts/'+c['combine']+'/combos', exist_ok=True)
   1582                         savetext('p/posts/'+c['combine']+'/combos/'+session.postid,session.user)
   1583                         os.makedirs(basedir+'p/heartrank/',exist_ok=True)
   1584                         try:
   1585                             l=len(os.listdir(basedir+'p/posts/'+session.postid+'/combos/'))
   1586                         except:
   1587                             l=0
   1588                         thedict={'combos':l}
   1589                         savejson('p/posts/'+c['combine']+'/meta',thedict)
   1590                         os.system('rm '+basedir+'p/comborank/'+session.postid+'-'+str(int(l)).zfill(16))
   1591                         os.system('cp '+basedir+'p/posts/'+session.postid+'/meta '+basedir+'p/comborank/'+session.postid+'-'+str(int(l)).zfill(16))
   1592                 description1 = text
   1593                 description2 = text2
   1594                 soundname = safe_filename(description1[0:27])
   1595                 thedict={'soundname':soundname}
   1596                 savejson('u/'+session.user+'/posts/'+session.postid+'/meta',thedict)
   1597                 os.system('cp -r '+basedir+'u/'+session.user+'/posts/'+session.postid+' '+basedir+'p/posts/')
   1598                 #symlinkthis(session.postid, session.user)
   1599                 #also zippit here!
   1600                 #os.system('zip -r '+basedir+'p/zipped/'+session.postid+'.zip '+basedir+'p/posts/'+session.postid )
   1601                 #os.system('cd '+basedir+'p/posts/ && zip -o -r '+session.postid+'.zip '+session.postid )
   1602                 #os.system('mv '+basedir+'p/posts/'+session.postid+'.zip '+basedir+'/p/zipped/')
   1603                 if zipandship == 'yes' or zipandship == 'True' or zipandship == 'y' or zipandship == 'Y':
   1604                     zippitandshippit(session.postid)
   1605                 raise web.seeother('/')
   1606             return rendersplash.editor(storage, text, text2, markdown, safe_filename, session.postid, i.public, logged(), session.user, i.combine, i.remix, session.saveto)
   1607 
   1608 class savepost:
   1609     def POST(self):
   1610         data = json.loads(web.data())
   1611         text = data.get("text", "")
   1612         text2 = data.get("text2", "")
   1613         if session.saveto != '':
   1614             savetext(text, text2)
   1615         else:
   1616             if session.postid == '':
   1617                 session.postid = hashlib.sha256(str(random.getrandbits(256)).encode('utf-8')).hexdigest()[9:36]
   1618                 os.makedirs(basedir+'u/'+session.user+'/posts/'+session.postid,exist_ok=True)
   1619                 thedict={'postid':session.postid, 'siteurl':siteurl, 'timeadded':formattime(), 'creator':session.user}
   1620                 savejson('u/'+session.user+'/posts/'+session.postid+'/meta',thedict)
   1621                 savetext('u/'+session.user+'/posts/'+session.postid+'/intro', text)
   1622                 savetext('u/'+session.user+'/posts/'+session.postid+'/post', text2)
   1623             else:
   1624                 os.makedirs(basedir+'u/'+session.user+'/posts/'+session.postid,exist_ok=True)
   1625                 thedict={'postid':session.postid, 'siteurl':siteurl, 'timeadded':formattime(), 'creator':session.user}
   1626                 savejson('u/'+session.user+'/posts/'+session.postid+'/meta',thedict)
   1627                 savetext('u/'+session.user+'/posts/'+session.postid+'/intro', text)
   1628                 savetext('u/'+session.user+'/posts/'+session.postid+'/post', text2)
   1629                 print('post saved!')
   1630         return "ok"  # simple response
   1631 
   1632 class imageapi:
   1633     def POST(self):
   1634         action = ''
   1635         data = json.loads(web.data())
   1636         image = data.get("image", "")
   1637         action = data.get("action", "")
   1638         original_thumb = Image.open(staticdir+'users/'+session.user+'/images/thumb/'+image) 
   1639         original_web = Image.open(staticdir+'users/'+session.user+'/images/web/'+image) 
   1640         if action != '':
   1641             if action == 'rotateright' and image != None:
   1642                 o_thumb=original_thumb.rotate(270)
   1643                 o_web=original_web.rotate(270)
   1644             if action == 'rotateleft' and image != None:
   1645                 o_thumb=original_thumb.rotate(90)
   1646                 o_web=original_web.rotate(90)
   1647             o_thumb.save(staticdir+'users/'+session.user+'/images/thumb/'+image)
   1648             o_web.save(staticdir+'users/'+session.user+'/images/web/'+image)
   1649         return  # simple response
   1650 
   1651 class rendered:
   1652     def GET(self):
   1653         description=''
   1654         description2=''
   1655         try:
   1656             description=loadtext('u/'+session.user+'/posts/'+session.postid+'/intro')
   1657         except:
   1658             pass
   1659         try:
   1660             description2=loadtext('u/'+session.user+'/posts/'+session.postid+'/post')
   1661         except:
   1662             pass
   1663         return markdown.markdown(description+'\n\n---\n\n'+description2)
   1664 
   1665 def get_new_frames(gif, scale):
   1666     new_frames = []
   1667     actual_frames = gif.n_frames
   1668     for frame in range(actual_frames):
   1669         gif.seek(frame)
   1670         new_frame = Image.new('RGBA', gif.size)
   1671         new_frame.paste(gif)
   1672         new_frame.thumbnail(scale, Image.Resampling.LANCZOS)
   1673         new_frames.append(new_frame)
   1674     return new_frames
   1675 
   1676 def save_new_gif(new_frames, old_gif_information, new_path):
   1677     new_frames[0].save(new_path,
   1678                        save_all = True,
   1679                        append_images = new_frames[1:],
   1680                        duration = old_gif_information['duration'],
   1681                        loop = old_gif_information['loop'],
   1682                        background = old_gif_information['background'],
   1683                        extension = old_gif_information['extension'] ,
   1684                        transparency = old_gif_information['transparency'])
   1685 
   1686 def scale_gif(path, scale, new_path=None):
   1687     gif = Image.open(path)
   1688     if not new_path:
   1689         new_path = path
   1690     old_gif_information = {
   1691         'loop': bool(gif.info.get('loop', 1)),
   1692         'duration': gif.info.get('duration', 40),
   1693         'background': gif.info.get('background', 223),
   1694         'extension': gif.info.get('extension', (b'NETSCAPE2.0')),
   1695         'transparency': gif.info.get('transparency', 223)
   1696     }
   1697     new_frames = get_new_frames(gif, scale)
   1698     save_new_gif(new_frames, old_gif_information, new_path)
   1699 
   1700 def getallmedia(media_dir, extensions):
   1701     files = []
   1702     for ext in extensions:
   1703         c=list(Path(media_dir).rglob('*.'+ext))
   1704         for a in c:
   1705             files.append(str(a))
   1706     print('wasassssup!?')
   1707     print(files)
   1708     return files
   1709 
   1710 def symlinkmedia(mediafile,postid,user): 
   1711     fixstatic = mediafile.split('/posts/'+postid+'/')[1]
   1712     symlinkfile = staticdir + 'users/' + user + '/'+fixstatic
   1713     symlinkdir=symlinkfile.rsplit('/',1)[0]
   1714     os.makedirs(symlinkdir, exist_ok=True)
   1715     os.system('ln -sf '+mediafile+' '+symlinkfile)
   1716 
   1717 def symlinkthis(postid,user):
   1718     extensions=['zip','pdf','txt','md','mp4','jpeg','jpg','png','gif','wav','flac','mp3','ogg']
   1719     mediafiles = getallmedia(basedir+'u/'+user+'/posts/'+postid,extensions)
   1720     for m in mediafiles:
   1721         symlinkmedia(m,postid,user)
   1722     print(mediafiles)
   1723 
   1724 #symlinkthis('cd449464ec08fc7aa967d9b2795')
   1725 
   1726 class upload:
   1727     def POST(self, mode):
   1728         if logged():
   1729             if session.postid == '':
   1730                 session.postid = hashlib.sha256(str(random.getrandbits(256)).encode('utf-8')).hexdigest()[9:36]
   1731                 os.makedirs(basedir+'u/'+session.user+'/posts/'+session.postid,exist_ok=True)
   1732             try:
   1733                 extensions=['zip','pdf','txt','md','mp4','jpeg','jpg','png','gif','wav','flac','mp3','ogg']
   1734                 saved_files = []
   1735                 # Best way for multiple files in web.py
   1736                 input_data = web.webapi.rawinput()
   1737                 uploaded = input_data.get('files')
   1738                 print('LETS GO!')
   1739                 print(session.user)
   1740                 print(input_data)
   1741                 print(uploaded)
   1742                 if not isinstance(uploaded, list):
   1743                     uploaded = [uploaded] if uploaded else []
   1744                 for f in uploaded:
   1745                     if f and hasattr(f, 'filename') and f.filename:
   1746                         # Sanitize filename a bit
   1747                         imgdir = basedir + 'u/' + session.user + '/temp/'
   1748                         os.system('mkdir -p ' + imgdir)
   1749                         safe_name = safe_filename(os.path.basename(f.filename))
   1750                         filepath = os.path.join(imgdir, safe_name)
   1751                         with open(filepath, 'wb') as out:
   1752                             out.write(f.file.read())  # Important: use .file.read()
   1753                         saved_files.append(safe_name)
   1754                         ##---------- UPLOAD SOUND ----------
   1755                         imgname=filepath.split('/')[-1] # splits the and chooses the last part (the filename with extension)
   1756                         filetype = imgname.split('.')[-1].lower()
   1757                         soundfile=safe_name
   1758                         print(filetype)
   1759                         if filetype == 'zip' and mode == 'ship':
   1760                             print('incoming!')
   1761                             usersound = basedir + 'u/' + session.user + '/zipped/'
   1762                             os.system('mkdir -p ' + usersound)
   1763                             os.system('mv ' + imgdir + soundfile + ' ' + usersound + soundfile)
   1764                             os.system('cd '+usersound+' && unzip -o '+soundfile+' -d '+basedir+'u/'+session.user+'/posts/')
   1765                             os.system('cd '+usersound+' && unzip -o '+soundfile+' -d '+basedir+'p/posts/')
   1766                             mediafiles = getallmedia(basedir+'p/posts/'+soundfile.split('.zip')[0],extensions)
   1767                             print('wowoweewaa')
   1768                             print(basedir+'p/posts/'+soundfile.split('.zip')[0])
   1769                             print(mediafiles)
   1770                             for m in mediafiles:
   1771                                 symlinkmedia(m,soundfile.split('.zip')[0],session.user)
   1772                         elif filetype == 'zip':
   1773                             usersound = basedir + 'u/' + session.user + '/posts/'+session.postid+'/docs/'
   1774                             os.system('mkdir -p ' + usersound)
   1775                             os.system('mv ' + imgdir + soundfile + ' ' + usersound + soundfile)
   1776                             symlinkmedia(usersound + soundfile,session.postid,session.user)
   1777                         elif filetype == 'pdf' or filetype == 'txt' or filetype == 'md':
   1778                             usersound = basedir + 'u/' + session.user + '/posts/'+session.postid+'/docs/'
   1779                             os.system('mkdir -p ' + usersound)
   1780                             os.system('mv ' + imgdir + soundfile + ' ' + usersound + soundfile)
   1781                             symlinkmedia(usersound + soundfile,session.postid,session.user)
   1782                         elif filetype == 'mp4':
   1783                             usersound = basedir + 'u/' + session.user + '/posts/'+session.postid+'/films/'
   1784                             os.system('mkdir -p ' + usersound)
   1785                             os.system('mv ' + imgdir + soundfile + ' ' + usersound + soundfile)
   1786                             symlinkmedia(usersound + soundfile,session.postid,session.user)
   1787                         elif filetype == 'jpeg' or filetype == 'jpg' or filetype == 'png' or filetype == 'gif':
   1788                             usersound = basedir + 'u/' + session.user + '/posts/'+session.postid+'/images/'
   1789                             os.system('mkdir -p ' + usersound)
   1790                             os.makedirs(usersound + 'web/', exist_ok=True)
   1791                             os.makedirs(usersound + 'thumb/', exist_ok=True)
   1792                             os.system('mv ' + imgdir + soundfile + ' ' + usersound + soundfile)
   1793                             symlinkmedia(usersound + soundfile,session.postid,session.user)
   1794                             if filetype == 'gif':
   1795                                 scale_gif(usersound+soundfile, [900,900], usersound+'web/'+soundfile)
   1796                                 scale_gif(usersound+soundfile, [300,300], usersound+'thumb/'+soundfile)
   1797                                 symlinkmedia(usersound + 'web/' + soundfile,session.postid,session.user)
   1798                                 symlinkmedia(usersound + 'thumb/' + soundfile,session.postid,session.user)
   1799                             else:
   1800                                 ##---------- OPEN FILE & CHEKC IF JPEG --------
   1801                                 image = Image.open(usersound + soundfile)
   1802                                 ##---------- RESIZE IMAGE -----------
   1803                                 image.thumbnail((900,900), Image.Resampling.LANCZOS)
   1804                                 image.save(usersound + 'web/' + soundfile)
   1805                                 symlinkmedia(usersound + 'web/' + soundfile,session.postid,session.user)
   1806                                 image.thumbnail((300,300), Image.Resampling.LANCZOS)
   1807                                 image.save(usersound + 'thumb/' + soundfile)
   1808                                 symlinkmedia(usersound + 'thumb/' + soundfile,session.postid,session.user)
   1809                                 print('images resized images')
   1810                         elif filetype == 'wav' or filetype == 'flac' or filetype == 'mp3' or filetype == 'ogg':
   1811                             usersound = basedir + 'u/' + session.user + '/posts/'+session.postid+'/sounds/'
   1812                             os.system('mkdir -p ' + usersound)
   1813                             os.system('mv ' + imgdir + soundfile + ' ' + usersound + soundfile)
   1814                             soundlenght = os.popen('mediainfo --Inform="General;%Duration%" ' + usersound + soundfile).read()
   1815                             print('sound lenght:' + str(soundlenght))
   1816                             soundtype = os.popen('mediainfo --Inform="General;%Format%" ' + usersound + soundfile).read()
   1817                             print(soundtype)
   1818                             if 'Ogg' in soundtype:
   1819                                 #os.system('ffmpeg -i '+usersound+soundfile+' '+usersound+soundname+'.wav')
   1820                                 print('ogg file found, converting to flac')
   1821                                 os.system('ffmpeg -i ' + usersound + soundfile +' '+ usersound + soundname + '.flac') 
   1822                                 print('converting to mp3')
   1823                                 os.system('ffmpeg -y -loglevel 1 -i ' + usersound + soundname + '.flac -c:a libmp3lame -b:a 192k ' + usersound + soundname + '.mp3') 
   1824                             if 'MPEG Audio' in soundtype:
   1825                                 print('mp3 file found, converting to flac')
   1826                                 os.system('ffmpeg -i ' + usersound + soundfile + ' ' + usersound + soundname + '.flac') 
   1827                                 print('converting to ogg')
   1828                                 os.system('ffmpeg -i ' + usersound + soundname +'.flac '+ usersound + soundname + '.ogg') 
   1829                                 print('Wave file found, converting to flac')
   1830                             if 'Wave' in soundtype:
   1831                                 print('Wave file found, converting to flac')
   1832                                 os.system('flac ' + usersound + soundfile + ' ' + usersound + soundname + '.flac') 
   1833                                 os.system('sox -V1 ' + usersound + soundfile + ' ' + usersound + soundname + '.ogg') 
   1834                                 os.system('ffmpeg -y -loglevel 1 -i ' + usersound + soundfile + ' -c:a libmp3lame -b:a 192k ' + usersound + soundname + '.mp3') 
   1835                             if 'FLAC' in soundtype:
   1836                                 print('FLAC file found, converting to mp3 and ogg')
   1837                                 os.system('sox -V1 ' + usersound + soundfile + ' ' + usersound + soundname + '.ogg') 
   1838                                 os.system('ffmpeg -y -loglevel 1 -i ' + usersound + soundfile + ' -c:a libmp3lame -b:a 192k ' + usersound + soundname + '.mp3')
   1839                         saved_files.append(safe_name)
   1840                         print(f"✅ Saved: {safe_name}")  # This will show in console for debugging
   1841                     else:
   1842                         print("⚠️ Skipped invalid file object")
   1843             except Exception as e:
   1844                 print("Upload error:", str(e))
   1845                 return f"❌ Error: {str(e)}"
   1846             if saved_files:
   1847                 return f"✅ Successfully uploaded {len(saved_files)} file(s): {', '.join(saved_files)}"
   1848             else:
   1849                 return "❌ No files received. Check console for details."
   1850         else:
   1851             print('access denied')
   1852 
   1853 class uploads:
   1854     def GET(self):
   1855         if logged():
   1856             uploaded = getfiles(staticdir+'upload/')
   1857             return render.uploads(uploaded)
   1858 
   1859 class pull:
   1860     def GET(self):
   1861         extensions=['zip','pdf','txt','md','mp4','jpeg','jpg','png','gif','wav','flac','mp3','ogg']
   1862         i = web.input(name=None,postid=None) #GET LIST WITH NAME JSON WHY NOT?! 
   1863         if i.name != None and i.postid != None:
   1864             print(i.name)
   1865             print(i.postid)
   1866             posts = get_dirs_by_time('p/posts/', reverse=True)
   1867             try:
   1868                 posts = posts[:posts.index(i.postid)] #MUST GET POST LIST FROM SERVER API INSTEAD DUDE
   1869             except:
   1870                 posts = get_dirs_by_time('p/posts/', reverse=True)
   1871             postdict={}
   1872             print(posts)
   1873             for p in posts:
   1874                 l=loadjson('p/posts/'+p+'/meta')
   1875                 postdict.update({l['postid']:{'postid': l['postid'], 'name': l['creator']}})
   1876             web.header('Content-Type', 'application/json')
   1877             web.header('Access-Control-Allow-Origin', '*')  # helps with browser/script access
   1878             return json.dumps(postdict)
   1879         else:
   1880             if logged():
   1881                 trusted=os.listdir(basedir+'r/trusted/'+session.user+'/')
   1882                 trustedlist=[]
   1883                 for t in trusted:
   1884                     trusted=loadjson('r/trusted/'+session.user+'/'+t)
   1885                     trustedlist.append(trusted)
   1886                 for t in trustedlist:
   1887                     print('pulling from '+t['servername'])
   1888                     #posts = getpostsfromse}erver(
   1889                     #postid = get_dirs_by_time('p/posts/', reverse=True)[0]
   1890                     postid=''
   1891                     pullposts = requests.get('https://'+t['servername']+'/pull?name='+t['user']+'&postid='+postid).json()
   1892                     print('https://'+t['servername']+'/pull?name='+t['user']+'&postid='+postid)
   1893                     print(pullposts)
   1894                     #zippandshipp=[]
   1895                     #if t['user']!=None:
   1896                     #    users = os.listdir(basedir+'r/users/')
   1897                     #    for r in users:
   1898                     #        if r['name']==t['user']:
   1899                     #            for p in posts:
   1900                     #                if p['creator']==r['name']:
   1901                     #                    pullnunzip.append(p)
   1902                     for p in pullposts:
   1903                         print('hold on pulling new posts')
   1904                         print(p)
   1905                         print(pullposts[p]['name'])
   1906                         os.system('wget -O '+basedir+'p/zipped/'+pullposts[p]['postid']+'.zip https://'+t['servername']+'/static/users/'+pullposts[p]['name']+'/zipped/'+pullposts[p]['postid']+'.zip')
   1907                         os.system('cd '+basedir+'p/zipped/ && unzip -o '+pullposts[p]['postid']+'.zip -d '+basedir+'u/'+session.user+'/posts/')
   1908                         os.system('cd '+basedir+'p/zipped/ && unzip -o '+pullposts[p]['postid']+'.zip -d '+basedir+'p/posts/')
   1909                         mediafiles = getallmedia(basedir+'p/posts/'+pullposts[p]['postid'],extensions)
   1910                         print('wowoweewaa')
   1911                         print(basedir+'p/posts/'+pullposts[p]['postid'])
   1912                         print(mediafiles)
   1913                         for m in mediafiles:
   1914                             symlinkmedia(m,pullposts[p]['postid'],session.user)
   1915                 #finally render rank
   1916                 rankrender()
   1917             else:
   1918                 print('no access!')
   1919 
   1920 class config:
   1921     def GET(self):
   1922         if logged():
   1923             if session.login == 5:
   1924                 i = web.input(do=None)
   1925                 if i.do!=None:
   1926                     raise web.seeother('/editor?saveto='+i.do)
   1927 
   1928 #Load from settings
   1929 standalone = settings.standaloneserver
   1930 if standalone == 'yes' or standalone == 'True' or standalone == 'y' or standalone == 'Y':
   1931     app.run()
   1932 else:
   1933     application = app.wsgifunc()