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))