diasporadiaries

a platform for writing stories with personal accounts and messages
git clone https://git.parazyd.org/diasporadiaries
Log | Files | Refs | Submodules | README | LICENSE

utils.py (14385B)


      1 # Copyright (c) 2019 Ivan Jelincic <parazyd@dyne.org>
      2 #
      3 # This file is part of diasporadiaries
      4 #
      5 # This program is free software: you can redistribute it and/or modify
      6 # it under the terms of the GNU Affero General Public License as published by
      7 # the Free Software Foundation, either version 3 of the License, or
      8 # (at your option) any later version.
      9 #
     10 # This program is distributed in the hope that it will be useful,
     11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 # GNU Affero General Public License for more details.
     14 #
     15 # You should have received a copy of the GNU Affero General Public License
     16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
     17 """
     18 Utility functions.
     19 """
     20 import json
     21 from hashlib import sha256
     22 from os import makedirs, listdir, remove
     23 from os.path import join, isfile
     24 from random import SystemRandom, shuffle
     25 from shutil import rmtree
     26 from string import ascii_lowercase, digits
     27 from time import gmtime, strftime, time
     28 
     29 from bcrypt import gensalt, hashpw
     30 from flask import Markup
     31 
     32 from config import sem
     33 from db import (sql_select_col_where, sql_select_col, sql_insert,
     34                 sql_delete_row_where)
     35 
     36 
     37 COUNTRYMAP = {}
     38 with open('world.json') as worldfile:
     39     JSON_DATA = json.load(worldfile)
     40 for cname in JSON_DATA:
     41     COUNTRYMAP[cname['alpha2']] = cname['name']
     42 COUNTRYMAP['xk'] = 'Kosovo'
     43 del JSON_DATA
     44 
     45 
     46 def randomstring(length):
     47     """
     48     Returns a random lowercase+digit string of given length.
     49     """
     50     return ''.join(SystemRandom().choice(ascii_lowercase+digits)
     51                    for _ in range(length))
     52 
     53 
     54 def getcountryname(ccode):
     55     """
     56     Returns a country name matching the given country code.
     57     """
     58     return COUNTRYMAP.get(ccode, 'Unknown countries')
     59 
     60 
     61 def fill_story(row):
     62     """
     63     Returns a story dict filled with the given row from the database.
     64     """
     65     return {
     66         'id': row[0],
     67         'name': row[1],
     68         'embark': row[2],
     69         'embarkname': getcountryname(row[2]),
     70         'disembark': row[3],
     71         'disembarkname': getcountryname(row[3]),
     72         'email': row[4],
     73         'city': row[5],
     74         'about': row[6],
     75         'story': row[7],
     76         'time': row[8],
     77         'visible': row[9],
     78         'deletekey': row[10],
     79         'abstract': row[11],
     80     }
     81 
     82 
     83 def get_story(story_id):
     84     """
     85     Returns a specific story matching the story_id.
     86     """
     87     if not story_id:
     88         return None
     89     res = sql_select_col_where('*', 'id', story_id)
     90     if not res:
     91         return None
     92     return fill_story(res[0])
     93 
     94 
     95 def get_recent_stories():
     96     """
     97     Returns a list of the 3 most recent stories in the database.
     98     """
     99     rows = sql_select_col_where('*', 'visible', 1)
    100     if len(rows) > 3:
    101         rows = rows[-3:]
    102 
    103     stories = []
    104     for i in rows:
    105         j = fill_story(i)
    106         stories.append(j)
    107 
    108     return stories[::-1]
    109 
    110 
    111 def get_multiple_stories(col, val, reverse=True):
    112     """
    113     Returns a list of stories where col=val.
    114     """
    115     rows = sql_select_col_where('*', col, val)
    116 
    117     stories = []
    118     for i in rows:
    119         j = fill_story(i)
    120         stories.append(j)
    121 
    122     if reverse:
    123         return stories[::-1]
    124     return stories
    125 
    126 
    127 def get_multiple_stories_filtered(col, val, param, reverse=True):
    128     """
    129     Returns a filtered list of stories where col=val.
    130 
    131     param is a tuple, where [0] will be compared to [1] for filtering.
    132     """
    133     stories = get_multiple_stories(col, val, reverse=reverse)
    134     # NOTE: Possible improvement, check when stories is empty and
    135     # act accordingly in country.html
    136 
    137     filtered_stories = []
    138     for i in stories:
    139         if i[param[0]] == param[1]:
    140             filtered_stories.append(i)
    141 
    142     return filtered_stories
    143 
    144 
    145 def get_multiple_users(col, val, reverse=False):
    146     """
    147     Returns a list of users where col=val.
    148     """
    149     if col and val:
    150         rows = sql_select_col_where('*', col, val, table='users')
    151     else:
    152         rows = sql_select_col('*', table='users')
    153 
    154     users = []
    155     for i in rows:
    156         j = fill_user_dict(i)
    157         users.append(j)
    158 
    159     if reverse:
    160         return users[::-1]
    161     return users
    162 
    163 
    164 def makenav(randomize=False):
    165     """
    166     Queries the disembark column for filling up the by-country dropdown
    167     navigation.
    168     """
    169     rows = sql_select_col_where('disembark', 'visible', 1)
    170 
    171     havec = [i[0] for i in rows]
    172     havec = list(set(havec))
    173     navlist = [(i, getcountryname(i)) for i in havec]
    174 
    175     if randomize:
    176         shuffle(navlist)
    177     return navlist
    178 
    179 
    180 def fill_user_dict(row):
    181     """
    182     Function to fill a dict with the given user's row from the database.
    183     """
    184     return {
    185         'id': row[0],
    186         'email': row[1],
    187         'name': row[2],
    188         'password': row[3],
    189         'cap': row[4],
    190         'first_seen': row[5],
    191         'is_active': row[6],
    192     }
    193 
    194 
    195 def find_user_by_id(user_id):
    196     """
    197     Queries the database for a specific user id and returns the user dict.
    198     """
    199     row = sql_select_col_where('*', 'id', user_id, table='users')
    200     if not row:
    201         return None
    202 
    203     return fill_user_dict(row[0])
    204 
    205 
    206 def find_user_by_email(email):
    207     """
    208     Queries the database for a user's email (username) and returns the user
    209     dict.
    210     """
    211     row = sql_select_col_where('*', 'email', email, table='users')
    212     if not row:
    213         return None
    214 
    215     return fill_user_dict(row[0])
    216 
    217 
    218 def validate_user(user, password):
    219     """
    220     Validates a user login.
    221     """
    222     if hashpw(password.encode(), user['password']) == user['password']:
    223         return True
    224 
    225     return False
    226 
    227 
    228 def make_profile(name, email):
    229     """
    230     Helper function to generate and insert a profile into the database.
    231     """
    232     if sql_select_col_where('email', 'email', email, table='users'):
    233         return None
    234 
    235     plain_pw = randomstring(24)
    236 
    237     # If it's the first user, make them an admin.
    238     cap = 2
    239     if sql_select_col('id', table='users'):
    240         cap = 0
    241 
    242     # hashed = bcrypt.hashpw(password, bcrypt.gensalt())
    243     # bcrypt.hashpw(plaintext, hashed) == hashed
    244     password = hashpw(plain_pw.encode(), gensalt())
    245 
    246     userargs = [
    247         None,
    248         email,
    249         name,
    250         password,
    251         cap,
    252         int(time()),
    253         0,
    254     ]
    255     sql_insert(userargs)
    256 
    257     makedirs('follows', exist_ok=True)
    258     with open(join('follows', email), 'w') as follow_file:
    259         json.dump([], follow_file)
    260 
    261     msgpath = join('messages', email)
    262     makedirs(msgpath, exist_ok=True)
    263 
    264     initial_user = find_user_by_id(1)
    265     if initial_user:
    266         welcome_msg = "Welcome to Diaspora Diaries! Enjoy your stay :)"
    267         with open(join(msgpath, initial_user['email']), 'w') as msgfile:
    268             json.dump([{'from': 'Diaspora Diaries', 'message': welcome_msg,
    269                         'time': int(time()), 'unread': 1}], msgfile)
    270 
    271     return plain_pw
    272 
    273 
    274 def get_latest_messages(user_id):
    275     """
    276     Gets the latest messages of a user to list them in a table.
    277     """
    278     msgpath = join('messages', user_id)
    279     ppl = listdir(msgpath)
    280     latest = []
    281     for i in ppl:
    282         if isfile(join(msgpath, i)):
    283             with open(join(msgpath, i)) as msgfile:
    284                 data = json.load(msgfile)
    285                 user = find_user_by_email(i)
    286                 if not user:
    287                     continue
    288                 data[-1]['message'] = data[-1]['message'][:64] + "..."
    289                 latest.append([user['name'], user['id'], data[-1]])
    290 
    291     return sorted(latest, key=lambda x: x[2]['time'], reverse=True)
    292 
    293 
    294 def get_messages(user_id, id_from):
    295     """
    296     Gets all messages in a single conversation.
    297     """
    298     email = sql_select_col_where('email', 'id', id_from, table='users')
    299     if email:
    300         email = email[0][0]
    301     else:
    302         return []
    303 
    304     msgpath = join('messages', user_id, email)
    305 
    306     data = []
    307     if isfile(msgpath):
    308         with open(msgpath) as msgfile:
    309             data = json.load(msgfile)
    310     else:
    311         with open(msgpath, 'w') as msgfile:
    312             json.dump(data, msgfile)
    313         return []
    314 
    315     messages = []
    316     for i in data:
    317         i['unread'] = 0
    318         messages.append(i)
    319 
    320     sem.acquire()
    321     with open(msgpath, 'w') as msgfile:
    322         json.dump(messages, msgfile)
    323     sem.release()
    324 
    325     return messages
    326 
    327 
    328 def send_message(id_to, msg, id_us):
    329     """
    330     Function for sending/recieving a message.
    331     """
    332     if not id_to or not msg or not id_us:
    333         return
    334     ours = find_user_by_id(id_us)
    335     them = find_user_by_id(id_to)
    336     if not ours or not them:
    337         return
    338 
    339     makedirs(join('messages', ours['email']), exist_ok=True)
    340     makedirs(join('messages', them['email']), exist_ok=True)
    341     our_msgpath = join('messages', ours['email'], them['email'])
    342     their_msgpath = join('messages', them['email'], ours['email'])
    343 
    344     msgdict = {
    345         'from': ours['name'],
    346         'message': Markup(msg).striptags(),
    347         'time': int(time()),
    348         'unread': 1,
    349     }
    350 
    351     ourdata = []
    352     theirdata = []
    353 
    354     if isfile(our_msgpath):
    355         with open(our_msgpath) as ourmsgs:
    356             ourdata = json.load(ourmsgs)
    357 
    358     sem.acquire()
    359     ourdata.append(msgdict)
    360     with open(our_msgpath, 'w') as ourmsgs:
    361         json.dump(ourdata, ourmsgs)
    362     sem.release()
    363 
    364     if our_msgpath == their_msgpath:
    365         return
    366 
    367     if isfile(their_msgpath):
    368         with open(their_msgpath) as theirmsgs:
    369             theirdata = json.load(theirmsgs)
    370 
    371     sem.acquire()
    372     theirdata.append(msgdict)
    373     with open(their_msgpath, 'w') as theirmsgs:
    374         json.dump(theirdata, theirmsgs)
    375     sem.release()
    376 
    377 
    378 def send_shout(id_to, msg, id_us):
    379     """
    380     Sends a shout to a user.
    381     """
    382     if not id_to or not msg or not id_us:
    383         return
    384     ours = find_user_by_id(id_us)
    385     them = find_user_by_id(id_to)
    386     if not ours or not them:
    387         return
    388 
    389     makedirs('shouts', exist_ok=True)
    390     shoutpath = (join('shouts', them['email']))
    391 
    392     m = sha256()
    393     m.update(Markup(msg).striptags().encode())
    394     msgdict = {
    395         'from': ours['name'],
    396         'email': ours['email'],
    397         'message': Markup(msg).striptags(),
    398         'time': int(time()),
    399         'id': m.hexdigest(),
    400     }
    401 
    402     theirdata = []
    403 
    404     if isfile(shoutpath):
    405         with open(shoutpath) as shouts_file:
    406             theirdata = json.load(shouts_file)
    407 
    408     sem.acquire()
    409     theirdata.append(msgdict)
    410     with open(shoutpath, 'w') as shouts_file:
    411         json.dump(theirdata, shouts_file)
    412     sem.release()
    413 
    414 
    415 def delete_shout(id_to, id_shout):
    416     """
    417     Deletes a specific shout.
    418     """
    419     if not id_to or not id_shout:
    420         return
    421     ours = find_user_by_id(id_to)
    422     if not ours:
    423         return
    424 
    425     shoutpath = join('shouts', ours['email'])
    426     if isfile(shoutpath):
    427         with open(shoutpath) as shouts_file:
    428             shouts = json.load(shouts_file)
    429 
    430         for i in shouts:
    431             if i['id'] == id_shout:
    432                 shouts.remove(i)
    433 
    434         sem.acquire()
    435         with open(shoutpath, 'w') as shouts_file:
    436             json.dump(shouts, shouts_file)
    437         sem.release()
    438 
    439 
    440 def get_shouts(email):
    441     """
    442     Gets all shouts for a user.
    443     """
    444     if not isfile(join('shouts', email)):
    445         return []
    446 
    447     shouts = []
    448     with open(join('shouts', email)) as shouts:
    449         shouts = json.load(shouts)
    450 
    451     return shouts[::-1]
    452 
    453 
    454 def follow_user(id_us, id_them):
    455     """
    456     Follows a user.
    457     """
    458     if not id_us or not id_them:
    459         return
    460     ours = find_user_by_id(id_us)
    461     them = find_user_by_id(id_them)
    462     if not ours or not them:
    463         return
    464 
    465     our_follow = join('follows', ours['email'])
    466     with open(our_follow) as follow_file:
    467         ourdata = json.load(follow_file)
    468 
    469     sem.acquire()
    470     ourdata.append(them['email'])
    471     with open(our_follow, 'w') as follow_file:
    472         json.dump(ourdata, follow_file)
    473     sem.release()
    474 
    475 
    476 def unfollow_user(id_us, id_them):
    477     """
    478     Unfollows a user.
    479     """
    480     if not id_us or not id_them:
    481         return
    482     ours = find_user_by_id(id_us)
    483     them = find_user_by_id(id_them)
    484     if not ours or not them:
    485         return
    486 
    487     our_follow = join('follows', ours['email'])
    488     with open(our_follow) as follow_file:
    489         ourdata = json.load(follow_file)
    490 
    491     if ourdata:
    492         while them['email'] in ourdata:
    493             ourdata.remove(them['email'])
    494 
    495     sem.acquire()
    496     with open(our_follow, 'w') as follow_file:
    497         json.dump(ourdata, follow_file)
    498     sem.release()
    499 
    500 
    501 def is_following(email_ours, email_them):
    502     """
    503     Function to check if we're following a specific user.
    504     """
    505     with open(join('follows', email_ours)) as follow_file:
    506         ourdata = json.load(follow_file)
    507 
    508     if email_them in ourdata:
    509         return True
    510 
    511     return False
    512 
    513 
    514 def get_following(email):
    515     """
    516     Gets all the users we are following.
    517     """
    518     with open(join('follows', email)) as follow_file:
    519         return json.load(follow_file)
    520 
    521 
    522 def delete_user(user_id):
    523     """
    524     Deletes a user and their messages directory.
    525     """
    526     user = find_user_by_id(user_id)
    527     rmtree(join('messages', user['email']), ignore_errors=True)
    528     remove(join('follows', user['email']))
    529     sql_delete_row_where('id', user_id, table='users')
    530 
    531 
    532 def get_profiles_from_stories(stories):
    533     """
    534     Map emails to ids from given stories.
    535     """
    536     profiles = {}
    537     for i in stories:
    538         if i['email']:
    539             uid = sql_select_col_where('id', 'email', i['email'],
    540                                        table='users')
    541             if uid:
    542                 profiles[i['email']] = uid[0][0]
    543 
    544     return profiles
    545 
    546 
    547 def parsetime(then):
    548     """
    549     Parse epoch to return human-readable time
    550     """
    551     ds = int(time()) - then
    552     dm = int(ds / 60)
    553 
    554     def f(x):
    555         if int(x) != 1:
    556             return 's'
    557         return ''
    558 
    559     if ds < 60:
    560         return '%d second%s ago' % (ds, f(ds))
    561     elif dm < 60:
    562         return '%d minute%s ago' % (dm, f(dm))
    563     elif dm < (24 * 60):
    564         return '%d hour%s ago' % (int(dm / 60), f(dm/60))
    565     elif dm < (24 * 60 * 7):
    566         return '%d day%s ago' % (int(dm / (60*24)), f(dm/(60*24)))
    567     elif dm < (24 * 60 * 31):
    568         return '%d week%s ago' % (int(dm / (60*24*7)), f(dm/(60*24*7)))
    569     elif dm < (24 * 60 * 365.25):
    570         return '%d month%s ago' % (int(dm / (60*24*30)), f(dm/(60*24*30)))
    571 
    572     return '%d year%s ago' % (int(dm / (60*24*365)), f(dm/(60*24*365)))
    573     # return strftime('%d.%m.%Y. %H:%M UTC', gmtime(then))