diaspora.py (15306B)
1 #!/usr/bin/env python3 2 # Copyright (c) 2019 Ivan Jelincic <parazyd@dyne.org> 3 # 4 # This file is part of diasporadiaries 5 # 6 # This program is free software: you can redistribute it and/or modify 7 # it under the terms of the GNU Affero General Public License as published by 8 # the Free Software Foundation, either version 3 of the License, or 9 # (at your option) any later version. 10 # 11 # This program is distributed in the hope that it will be useful, 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 # GNU Affero General Public License for more details. 15 # 16 # You should have received a copy of the GNU Affero General Public License 17 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 """ 19 Main diasporadiaries module 20 """ 21 from random import choice 22 from time import time 23 24 from bcrypt import hashpw, gensalt 25 from flask import Markup, Flask, render_template, request, redirect 26 from flask_login import (LoginManager, UserMixin, login_required, login_user, 27 logout_user, current_user) 28 29 from config import SECRET_KEY 30 from db import (sql_delete_row_where, sql_update_row_where, sql_insert, 31 sql_select_col, sql_select_col_where) 32 from utils import (get_story, makenav, randomstring, getcountryname, 33 get_multiple_stories, get_multiple_stories_filtered, 34 make_profile, find_user_by_id, find_user_by_email, 35 validate_user, get_multiple_users, get_messages, 36 get_latest_messages, send_message, delete_user, 37 get_recent_stories, follow_user, unfollow_user, 38 is_following, get_following, send_shout, get_shouts, 39 get_profiles_from_stories, parsetime, delete_shout) 40 41 42 app = Flask(__name__) 43 app.config['SECRET_KEY'] = SECRET_KEY 44 app.add_template_global(makenav) 45 app.add_template_global(parsetime) 46 47 login_manager = LoginManager() 48 login_manager.init_app(app) 49 login_manager.login_view = 'login' 50 51 52 class LoginUser(UserMixin): 53 """ 54 Class for a user 55 """ 56 # TODO: Find out if it's possible to do less SQL queries. 57 def __init__(self, uid): 58 """ Init """ 59 self.id = uid 60 61 @property 62 def username(self): 63 """ Username """ 64 user = self.get_user() 65 return user['email'] 66 67 @property 68 def name(self): 69 """ Name """ 70 user = self.get_user() 71 return user['name'] 72 73 @property 74 def is_admin(self): 75 """ Check capabilities """ 76 user = self.get_user() 77 if user['cap'] == 2: 78 return True 79 return False 80 81 def get_user(self): 82 """ Get user struct """ 83 return find_user_by_id(self.id) 84 85 86 @login_manager.user_loader 87 def load_user(user_id): 88 """ 89 flask-security function for loading the LoginUser class for a user. 90 """ 91 if not find_user_by_id(user_id): 92 return None 93 return LoginUser(uid=user_id) 94 95 96 @app.route('/login', methods=['GET', 'POST']) 97 def login(): 98 """ 99 User login route. 100 """ 101 if request.method == 'POST': 102 if request.form.get('42'): 103 return render_template('fail.html', msg='You robot!') 104 105 username = request.form['username'] 106 password = request.form['password'] 107 108 user = find_user_by_email(username) 109 if not user: 110 return render_template('fail.html', msg='No such user.') 111 112 if validate_user(user, password): 113 login_user(LoginUser(user['id']), remember=True) 114 115 if user['is_active'] == 0: 116 return redirect('/changepass') 117 118 nxt = request.args.get('next') 119 if not nxt: 120 return redirect('/') 121 else: 122 return render_template('fail.html', msg='Incorrect password.') 123 124 return render_template('login.html') 125 126 127 @app.route('/changepass', methods=['GET', 'POST']) 128 @login_required 129 def changepass(): 130 """ 131 Route for changing passwords. 132 """ 133 user = find_user_by_email(current_user.username) 134 135 if request.method != 'POST': 136 return render_template('change_pass.html', first=user['is_active']) 137 138 if request.form['42']: 139 return render_template('fail.html', msg='You robot!') 140 141 old_in_db = user['password'] 142 old_in_pg = request.form.get('oldpassword') 143 new_in_pg = request.form.get('newpassword') 144 if not old_in_pg or not new_in_pg: 145 return render_template('fail.html', 146 msg='Please input both old and new passwords!') 147 148 old_hashed = hashpw(old_in_pg.encode(), old_in_db) 149 if old_hashed == old_in_db: 150 hashed = hashpw(new_in_pg.encode(), gensalt()) 151 sql_update_row_where([('password', hashed), ('is_active', 1)], 152 'email', user['email'], table='users') 153 else: 154 return render_template('fail.html', msg='Old password is incorrect.') 155 156 return render_template('success.html', msg='Password change success!') 157 158 159 @app.route('/logout') 160 @login_required 161 def logout(): 162 """ 163 User logout route. 164 """ 165 logout_user() 166 return render_template('success.html', msg='Successfully logged out!') 167 168 169 @app.route('/messages') 170 @login_required 171 def messages(): 172 """ 173 Route for users' messages. 174 """ 175 id_from = request.args.get('from') 176 if id_from: 177 msgs = get_messages(current_user.username, id_from) 178 return render_template('messages.html', messages=msgs[::-1], 179 single=True, them=find_user_by_id(id_from)) 180 181 msgs = get_latest_messages(current_user.username) 182 return render_template('messages.html', messages=msgs) 183 184 185 @app.route('/follow', methods=['POST']) 186 @login_required 187 def follow(): 188 """ 189 Route for following a user. 190 """ 191 192 if request.form.get('Act') == 'follow': 193 follow_user(current_user.id, request.form.get('Id')) 194 elif request.form.get('Act') == 'unfollow': 195 unfollow_user(current_user.id, request.form.get('Id')) 196 197 if request.form.get('From') == '/following': 198 return redirect('/following') 199 200 return redirect('/profile?id=%s' % request.form.get('Id')) 201 202 203 @app.route('/sendmsg', methods=['POST']) 204 @login_required 205 def sendmsg(): 206 """ 207 Route for sending a single message. 208 """ 209 send_message(request.form.get('Id'), request.form.get('Message'), 210 request.form.get('Us')) 211 return redirect('/messages?from=%s' % request.form.get('Id')) 212 213 214 @app.route('/sendshout', methods=['POST']) 215 @login_required 216 def sendshout(): 217 """ 218 Route for leaving a shout. 219 """ 220 send_shout(request.form.get('Id'), request.form.get('Message'), 221 request.form.get('Us')) 222 return redirect('/profile?id=%s' % request.form.get('Id')) 223 224 225 @app.route('/delshout', methods=['POST']) 226 @login_required 227 def delshout(): 228 """ 229 Route for deleting a shout. 230 """ 231 delete_shout(request.form.get('Id'), request.form.get('ShoutId')) 232 return redirect('/profile?id=%s' % request.form.get('Id')) 233 234 235 @app.route('/write', methods=['GET', 'POST']) 236 def write(): 237 """ 238 Route for writing a story. 239 """ 240 if request.method != 'POST': 241 return render_template('write.html') 242 243 if request.form.get('42'): 244 return render_template('fail.html', msg='Are you a robot?') 245 246 delkey = randomstring(32) 247 storyargs = [ 248 None, 249 request.form.get('Name', 'Anonymous writer'), 250 request.form.get('Embark', 'nn'), 251 request.form.get('Disembark', 'nn'), 252 request.form.get('Email'), 253 request.form.get('City'), 254 request.form.get('About'), 255 request.form.get('Story', 'foobar'), 256 int(time()), 257 0, 258 delkey, 259 Markup(request.form['Story']).striptags()[:127], 260 ] 261 sql_insert(storyargs) 262 263 new_account = False 264 if request.form.get('Email'): 265 new_account = make_profile(request.form.get('Name', 'Anonymous user'), 266 request.form.get('Email')) 267 268 return render_template('success_write.html', delkey=delkey, 269 new=new_account) 270 271 272 @app.route('/edit', methods=['GET', 'POST']) 273 @login_required 274 def edit(): 275 """ 276 Route for editing and redacting. 277 """ 278 if request.method != 'POST': 279 story = get_story(request.args.get('id')) 280 if story: 281 if current_user.is_admin or current_user.username == story['email']: 282 return render_template('edit.html', story=story) 283 return render_template('fail.html', msg='Unauthorized.') 284 285 return render_template('fail.html', msg='No story with this story id.') 286 287 vals = [ 288 ('name', request.form.get('Name', 'Anonymous user')), 289 ('embark', request.form.get('Embark', 'nn')), 290 ('disembark', request.form.get('Disembark', 'nn')), 291 ('email', request.form.get('Email')), 292 ('city', request.form.get('City')), 293 ('story', request.form.get('Story', 'foo')), 294 ('about', request.form.get('About')), 295 ('abstract', request.form.get('Abstract', 'foo')), 296 ] 297 298 if request.form.get('Id'): 299 story = get_story(request.form.get('Id')) 300 if current_user.is_admin or current_user.username == story['email']: 301 sql_update_row_where(vals, 'id', request.form.get('Id')) 302 return render_template('success.html', msg='Story redacted!') 303 304 return render_template('fail.html', msg='Redaction failed.') 305 306 307 @app.route('/view') 308 def view(): 309 """ 310 Route for viewing a single story. 311 If no story is specified, it renders a random story. 312 """ 313 story_id = request.args.get('id') 314 if not story_id: 315 story_id = choice(sql_select_col('id'))[0] 316 317 story = get_story(story_id) 318 user = find_user_by_email(story['email']) 319 320 return render_template('view.html', story=story, user=user) 321 322 323 @app.route('/delete') 324 def delete(): 325 """ 326 Route for deleting a specific story. 327 """ 328 delkey = request.args.get('key') 329 if delkey: 330 story_id = sql_select_col_where('id', 'deletekey', delkey) 331 if story_id: 332 story_id = story_id[0][0] 333 sql_delete_row_where('id', story_id) 334 return render_template('success.html', 335 msg="We're sorry you deleted your story.") 336 337 return render_template('fail.html', 338 msg='No story is linked to your delete key.') 339 340 341 @app.route('/country') 342 def country(): 343 """ 344 Route for viewing stories for a specific country. 345 If no country is given, it will show a random country. 346 """ 347 ccode = request.args.get('id') 348 if not ccode: 349 ccode = choice(sql_select_col('disembark'))[0] 350 351 ccfrom = request.args.get('from') 352 filtered = None 353 if ccfrom: 354 filtered = get_multiple_stories_filtered('disembark', ccode, 355 ('embark', ccfrom)) 356 357 stories = get_multiple_stories('disembark', ccode) 358 profiles = get_profiles_from_stories(stories) 359 360 clist = [(i['embark'], i['embarkname']) for i in stories] 361 clist = list(set(clist)) 362 363 return render_template('country.html', country=getcountryname(ccode), 364 cc=ccode, stories=stories, clist=clist, 365 filtered_stories=filtered, profiles=profiles) 366 367 368 @app.route('/profile') 369 def profile(): 370 """ 371 Route for viewing stories by a specific person. The persons 372 are differed by their emails. 373 """ 374 uid = request.args.get('id') 375 if uid: 376 user = find_user_by_id(uid) 377 if user: 378 stories = get_multiple_stories_filtered('email', user['email'], 379 ('visible', 1)) 380 381 fol = False 382 if current_user.is_active: 383 fol = is_following(current_user.username, user['email']) 384 return render_template('profile.html', name=user['name'], uid=uid, 385 stories=stories, profiles={}, 386 shouts=get_shouts(user['email']), 387 is_following=fol) 388 389 return render_template('fail.html', msg='No such profile.') 390 391 392 @app.route('/following') 393 @login_required 394 def following(): 395 """ 396 Route for listing the users a user is following. 397 """ 398 fol = [] 399 for i in get_following(current_user.username): 400 j = find_user_by_email(i) 401 if j: 402 cnt = [] 403 countries = sql_select_col_where('disembark', 'email', j['email']) 404 for k in countries: 405 cnt.append((k[0], getcountryname(k[0]))) 406 fol.append([j['id'], j['name'], cnt]) 407 408 return render_template('following.html', following=fol) 409 410 411 @app.route('/dashboard') 412 @login_required 413 def dashboard(): 414 """ 415 Route for dashboard/admin view. 416 """ 417 if not current_user.is_admin: 418 return render_template('fail.html', msg="Unauthorized.") 419 420 fparam = {'pending': 0, 'published': 1} 421 422 add_id = request.args.get('approveid') 423 if add_id: 424 sql_update_row_where([('visible', 1)], 'id', add_id) 425 return render_template('success.html', msg='Story approved!') 426 427 hide_id = request.args.get('hideid') 428 if hide_id: 429 sql_update_row_where([('visible', 0)], 'id', hide_id) 430 return render_template('success.html', msg='Story hidden.') 431 432 act = request.args.get('list', 'pending') 433 434 ccto = request.args.get('to') 435 filtered = None 436 if ccto: 437 filtered = get_multiple_stories_filtered('visible', fparam[act], 438 ('disembark', ccto)) 439 440 stories = get_multiple_stories('visible', fparam[act]) 441 profiles = get_profiles_from_stories(stories) 442 443 clist = [(i['disembark'], i['disembarkname']) for i in stories] 444 clist = list(set(clist)) 445 446 return render_template('dashboard.html', stories=stories, 447 filtered_stories=filtered, profiles=profiles, 448 listtype=act, clist=clist, ccto=ccto) 449 450 451 @app.route('/users', methods=['GET', 'POST']) 452 @login_required 453 def users(): 454 """ 455 Route for users view. 456 """ 457 if not current_user.is_admin: 458 return render_template('fail.html', msg='Unauthorized.') 459 460 if request.method == 'POST': 461 vals = [] 462 if request.form.get('password'): 463 vals.append(('password', 464 hashpw(request.form.get('password').encode(), 465 gensalt()))) 466 if request.form.get('cap'): 467 vals.append(('cap', request.form.get('cap'))) 468 469 if vals and request.form.get('id'): 470 sql_update_row_where(vals, 'id', request.form.get('id'), 471 table='users') 472 473 if request.args.get('delid'): 474 delete_user(request.args.get('delid')) 475 476 # userlist = get_multiple_users('is_active', 1) 477 userlist = get_multiple_users(None, None) 478 return render_template('users.html', users=userlist) 479 480 481 @app.route('/contact') 482 def contact(): 483 """ 484 Contact route 485 """ 486 return render_template('contact.html') 487 488 489 @app.route('/') 490 def main(): 491 """ 492 Main route, the homepage. 493 """ 494 stories = get_recent_stories() 495 profiles = get_profiles_from_stories(stories) 496 return render_template('index.html', stories=stories, profiles=profiles)