Browse Source

Can show the right follow link; Formatting

Dashie der otter 3 months ago
parent
commit
124c9263ca
43 changed files with 274 additions and 755 deletions
  1. 1
    1
      .isort.cfg
  2. 11
    35
      activitypub/backend.py
  3. 2
    5
      activitypub/utils.py
  4. 10
    46
      app.py
  5. 1
    10
      controllers/admin.py
  6. 13
    56
      controllers/albums.py
  7. 11
    50
      controllers/api/v1/activitypub.py
  8. 1
    3
      controllers/api/v1/nodeinfo.py
  9. 7
    24
      controllers/api/v1/well_known.py
  10. 53
    42
      controllers/search.py
  11. 18
    74
      controllers/sound.py
  12. 13
    60
      controllers/users.py
  13. 1
    5
      dbseed.py
  14. 2
    6
      forms.py
  15. 3
    7
      migrations/env.py
  16. 2
    6
      migrations/versions/00_795db5a5e99b_.py
  17. 1
    3
      migrations/versions/01_da3273ca0f0f_.py
  18. 1
    3
      migrations/versions/06_14ecb77e9482_.py
  19. 1
    3
      migrations/versions/08_fc50eb4e9b34_.py
  20. 1
    3
      migrations/versions/09_aeb6c1345690_.py
  21. 1
    3
      migrations/versions/10_a901f35d5686_.py
  22. 1
    3
      migrations/versions/11_f41ac4617ef6_.py
  23. 2
    7
      migrations/versions/17_01e6d591fe6f_.py
  24. 1
    3
      migrations/versions/19_98b863ae920c_.py
  25. 2
    12
      migrations/versions/20_691eaff10a88_.py
  26. 2
    12
      migrations/versions/21_abf095d9c9f3_.py
  27. 2
    6
      migrations/versions/22_835be4f73770_.py
  28. 2
    7
      migrations/versions/23_2ec399782f52_.py
  29. 2
    6
      migrations/versions/25_32f48de123f3_.py
  30. 2
    10
      migrations/versions/27_687d40646d63_.py
  31. 5
    26
      migrations/versions/28_2e83f405e9a4_.py
  32. 1
    4
      migrations/versions/31_7ce00beb5b0a_.py
  33. 39
    101
      models.py
  34. 2
    0
      pyproject.toml
  35. 1
    0
      setup.cfg
  36. 25
    0
      templates/search/local_users_auth.jinja2
  37. 0
    0
      templates/search/local_users_unauth.jinja2
  38. 6
    1
      templates/search/remote_user.jinja2
  39. 1
    3
      tests/helpers.py
  40. 3
    13
      tests/test_c_users.py
  41. 6
    21
      tests/test_wellknown.py
  42. 7
    48
      utils.py
  43. 8
    27
      workers.py

+ 1
- 1
.isort.cfg View File

@@ -1,3 +1,3 @@
1 1
 [settings]
2 2
 line_length=120
3
-force_single_line=true
3
+force_single_line=false

+ 11
- 35
activitypub/backend.py View File

@@ -45,9 +45,7 @@ class Reel2BitsBackend(ap.Backend):
45 45
     def note_url(self, obj_id: str):
46 46
         return f"{self.base_url()}/note/{obj_id}"
47 47
 
48
-    def new_follower(
49
-        self, activity: ap.BaseActivity, as_actor: ap.Person, follow: ap.Follow
50
-    ) -> None:
48
+    def new_follower(self, activity: ap.BaseActivity, as_actor: ap.Person, follow: ap.Follow) -> None:
51 49
         current_app.logger.info("new follower")
52 50
 
53 51
         db_actor = Actor.query.filter(Actor.url == as_actor.id).first()
@@ -59,9 +57,7 @@ class Reel2BitsBackend(ap.Backend):
59 57
             current_app.logger.error(f"cannot find follow {follow!r}")
60 58
             return
61 59
 
62
-        current_app.logger.info(
63
-            f"{db_actor.name} wanted " f"to follow {db_follow.name}"
64
-        )
60
+        current_app.logger.info(f"{db_actor.name} wanted " f"to follow {db_follow.name}")
65 61
 
66 62
         db_actor.follow(activity.id, db_follow)
67 63
         db.session.commit()
@@ -100,9 +96,7 @@ class Reel2BitsBackend(ap.Backend):
100 96
         # fetch the activity
101 97
         activity = Activity.query.filter(Activity.url == undo_activity).first()
102 98
         if not activity:
103
-            current_app.logger.error(
104
-                f"cannot find activity" f" to undo: {undo_activity}"
105
-            )
99
+            current_app.logger.error(f"cannot find activity" f" to undo: {undo_activity}")
106 100
             return
107 101
 
108 102
         # Parse the activity
@@ -171,13 +165,9 @@ class Reel2BitsBackend(ap.Backend):
171 165
         domain = urlparse(ap_actor.id)
172 166
         current_app.logger.debug(f"actor.id=={ap_actor.__dict__}")
173 167
 
174
-        current_app.logger.debug(
175
-            f"actor domain {domain.netloc} and " f"name {ap_actor.preferredUsername}"
176
-        )
168
+        current_app.logger.debug(f"actor domain {domain.netloc} and " f"name {ap_actor.preferredUsername}")
177 169
 
178
-        actor = Actor.query.filter(
179
-            Actor.domain == domain.netloc, Actor.name == ap_actor.preferredUsername
180
-        ).first()
170
+        actor = Actor.query.filter(Actor.domain == domain.netloc, Actor.name == ap_actor.preferredUsername).first()
181 171
 
182 172
         if not actor:
183 173
             actor = create_remote_actor(ap_actor)
@@ -256,9 +246,7 @@ def process_new_activity(activity: ap.BaseActivity) -> None:
256 246
             if note.inReplyTo:
257 247
                 try:
258 248
                     reply = ap.fetch_remote_activity(note.inReplyTo)
259
-                    if (
260
-                        reply.id.startswith(id) or reply.has_mention(id)
261
-                    ) and activity.is_public():
249
+                    if (reply.id.startswith(id) or reply.has_mention(id)) and activity.is_public():
262 250
                         # The reply is public "local reply", forward the
263 251
                         # reply (i.e. the original activity) to the
264 252
                         # original recipients
@@ -284,9 +272,7 @@ def process_new_activity(activity: ap.BaseActivity) -> None:
284 272
                 should_forward = False
285 273
 
286 274
         elif activity.has_type(ap.ActivityType.DELETE):
287
-            note = Activity.query.filter(
288
-                Activity.id == activity.get_object().id
289
-            ).first()
275
+            note = Activity.query.filter(Activity.id == activity.get_object().id).first()
290 276
             if note and note["meta"].get("forwarded", False):
291 277
                 # If the activity was originally forwarded, forward the
292 278
                 # delete too
@@ -319,13 +305,9 @@ def process_new_activity(activity: ap.BaseActivity) -> None:
319 305
         current_app.logger.info(f"new activity {activity.id} processed")
320 306
 
321 307
     except (ActivityGoneError, ActivityNotFoundError):
322
-        current_app.logger.exception(
323
-            f"failed to process new activity" f" {activity.id}"
324
-        )
308
+        current_app.logger.exception(f"failed to process new activity" f" {activity.id}")
325 309
     except Exception as err:
326
-        current_app.logger.exception(
327
-            f"failed to process new activity" f" {activity.id}"
328
-        )
310
+        current_app.logger.exception(f"failed to process new activity" f" {activity.id}")
329 311
 
330 312
 
331 313
 # TODO, this must move to Dramatiq queueing
@@ -371,9 +353,7 @@ def finish_inbox_processing(activity: ap.BaseActivity) -> None:
371 353
     except (ActivityGoneError, ActivityNotFoundError, NotAnActivityError):
372 354
         current_app.logger.exception(f"no retry")
373 355
     except Exception as err:
374
-        current_app.logger.exception(
375
-            f"failed to cache attachments for" f" {activity.id}"
376
-        )
356
+        current_app.logger.exception(f"failed to cache attachments for" f" {activity.id}")
377 357
 
378 358
 
379 359
 def post_to_outbox(activity: ap.BaseActivity) -> str:
@@ -468,11 +448,7 @@ def post_to_remote_inbox(payload: str, to: str) -> None:
468 448
             to,
469 449
             data=json.dumps(signed_payload),
470 450
             auth=signature_auth,
471
-            headers={
472
-                "Content-Type": HEADERS[1],
473
-                "Accept": HEADERS[1],
474
-                "User-Agent": backend.user_agent(),
475
-            },
451
+            headers={"Content-Type": HEADERS[1], "Accept": HEADERS[1], "User-Agent": backend.user_agent()},
476 452
         )
477 453
         current_app.logger.info("resp=%s", resp)
478 454
         current_app.logger.info("resp_body=%s", resp.text)

+ 2
- 5
activitypub/utils.py View File

@@ -47,13 +47,10 @@ def add_extra_collection(item: Dict[str, Any]) -> Dict[str, Any]:
47 47
         return item
48 48
 
49 49
     item["object"]["replies"] = embed_collection(
50
-        item.get("meta", {}).get("count_direct_reply", 0),
51
-        f'{item["remote_id"]}/replies',
50
+        item.get("meta", {}).get("count_direct_reply", 0), f'{item["remote_id"]}/replies'
52 51
     )
53 52
 
54
-    item["object"]["likes"] = embed_collection(
55
-        item.get("meta", {}).get("count_like", 0), f'{item["remote_id"]}/likes'
56
-    )
53
+    item["object"]["likes"] = embed_collection(item.get("meta", {}).get("count_like", 0), f'{item["remote_id"]}/likes')
57 54
 
58 55
     item["object"]["shares"] = embed_collection(
59 56
         item.get("meta", {}).get("count_boost", 0), f'{item["remote_id"]}/shares'

+ 10
- 46
app.py View File

@@ -4,16 +4,7 @@ import os
4 4
 import subprocess
5 5
 from logging.handlers import RotatingFileHandler
6 6
 from flask_babelex import gettext, Babel
7
-from flask import (
8
-    Flask,
9
-    render_template,
10
-    g,
11
-    send_from_directory,
12
-    jsonify,
13
-    safe_join,
14
-    request,
15
-    flash,
16
-)
7
+from flask import Flask, render_template, g, send_from_directory, jsonify, safe_join, request, flash
17 8
 from flask_bootstrap import Bootstrap
18 9
 from flask_mail import Mail
19 10
 from flask_migrate import Migrate
@@ -35,13 +26,7 @@ from controllers.api.v1.activitypub import bp_ap
35 26
 
36 27
 from forms import ExtendedRegisterForm
37 28
 from models import db, Config, user_datastore, Role, create_actor
38
-from utils import (
39
-    InvalidUsage,
40
-    is_admin,
41
-    duration_elapsed_human,
42
-    duration_song_human,
43
-    add_user_log,
44
-)
29
+from utils import InvalidUsage, is_admin, duration_elapsed_human, duration_song_human, add_user_log
45 30
 
46 31
 import texttable
47 32
 from flask_debugtoolbar import DebugToolbarExtension
@@ -92,12 +77,8 @@ def create_app(config_filename="config.py"):
92 77
 
93 78
     # Logging
94 79
     if not app.debug:
95
-        formatter = logging.Formatter(
96
-            "%(asctime)s %(levelname)s: %(message)s " "[in %(pathname)s:%(lineno)d]"
97
-        )
98
-        file_handler = RotatingFileHandler(
99
-            "%s/errors_app.log" % os.getcwd(), "a", 1000000, 1
100
-        )
80
+        formatter = logging.Formatter("%(asctime)s %(levelname)s: %(message)s " "[in %(pathname)s:%(lineno)d]")
81
+        file_handler = RotatingFileHandler("%s/errors_app.log" % os.getcwd(), "a", 1000000, 1)
101 82
         file_handler.setLevel(logging.DEBUG)
102 83
         file_handler.setFormatter(formatter)
103 84
         app.logger.addHandler(file_handler)
@@ -115,10 +96,7 @@ def create_app(config_filename="config.py"):
115 96
 
116 97
     # Setup Flask-Security
117 98
     security = Security(  # noqa: F841
118
-        app,
119
-        user_datastore,
120
-        register_form=ExtendedRegisterForm,
121
-        confirm_register_form=ExtendedRegisterForm,
99
+        app, user_datastore, register_form=ExtendedRegisterForm, confirm_register_form=ExtendedRegisterForm
122 100
     )
123 101
 
124 102
     @FlaskSecuritySignals.password_reset.connect_via(app)
@@ -126,17 +104,13 @@ def create_app(config_filename="config.py"):
126 104
     def log_password_reset(sender, user):
127 105
         if not user:
128 106
             return
129
-        add_user_log(
130
-            user.id, user.id, "user", "info", "Your password has been changed !"
131
-        )
107
+        add_user_log(user.id, user.id, "user", "info", "Your password has been changed !")
132 108
 
133 109
     @FlaskSecuritySignals.reset_password_instructions_sent.connect_via(app)
134 110
     def log_reset_password_instr(sender, user, token):
135 111
         if not user:
136 112
             return
137
-        add_user_log(
138
-            user.id, user.id, "user", "info", "Password reset instructions sent."
139
-        )
113
+        add_user_log(user.id, user.id, "user", "info", "Password reset instructions sent.")
140 114
 
141 115
     @FlaskSecuritySignals.user_registered.connect_via(app)
142 116
     def create_actor_for_registered_user(app, user, confirm_token):
@@ -236,12 +210,7 @@ def create_app(config_filename="config.py"):
236 210
 
237 211
     @app.errorhandler(410)
238 212
     def err_gone(msg):
239
-        pcfg = {
240
-            "title": gettext("Whoops, something failed."),
241
-            "error": 410,
242
-            "message": gettext("Gone"),
243
-            "e": msg,
244
-        }
213
+        pcfg = {"title": gettext("Whoops, something failed."), "error": 410, "message": gettext("Gone"), "e": msg}
245 214
         return render_template("error_page.jinja2", pcfg=pcfg), 410
246 215
 
247 216
     if not app.debug:
@@ -289,9 +258,7 @@ def create_app(config_filename="config.py"):
289 258
         """Create an user"""
290 259
         username = click.prompt("Username", type=str)
291 260
         email = click.prompt("Email", type=str)
292
-        password = click.prompt(
293
-            "Password", type=str, hide_input=True, confirmation_prompt=True
294
-        )
261
+        password = click.prompt("Password", type=str, hide_input=True, confirmation_prompt=True)
295 262
         while True:
296 263
             role = click.prompt("Role [admin/user]", type=str)
297 264
             if role == "admin" or role == "user":
@@ -302,10 +269,7 @@ def create_app(config_filename="config.py"):
302 269
             if not role:
303 270
                 raise click.UsageError("Roles not present in database")
304 271
             u = user_datastore.create_user(
305
-                name=username,
306
-                email=email,
307
-                password=encrypt_password(password),
308
-                roles=[role],
272
+                name=username, email=email, password=encrypt_password(password), roles=[role]
309 273
             )
310 274
 
311 275
             actor = create_actor(u)

+ 1
- 10
controllers/admin.py View File

@@ -1,13 +1,4 @@
1
-from flask import (
2
-    Blueprint,
3
-    render_template,
4
-    request,
5
-    redirect,
6
-    url_for,
7
-    flash,
8
-    Response,
9
-    json,
10
-)
1
+from flask import Blueprint, render_template, request, redirect, url_for, flash, Response, json
11 2
 from flask_babelex import gettext
12 3
 from flask_security import login_required
13 4
 

+ 13
- 56
controllers/albums.py View File

@@ -1,13 +1,4 @@
1
-from flask import (
2
-    Blueprint,
3
-    render_template,
4
-    request,
5
-    redirect,
6
-    url_for,
7
-    flash,
8
-    Response,
9
-    json,
10
-)
1
+from flask import Blueprint, render_template, request, redirect, url_for, flash, Response, json
11 2
 from flask_babelex import gettext
12 3
 from flask_security import login_required, current_user
13 4
 
@@ -36,20 +27,12 @@ def new(username):
36 27
         db.session.commit()
37 28
 
38 29
         # log
39
-        add_user_log(
40
-            rec.id,
41
-            rec.user_id,
42
-            "albums",
43
-            "info",
44
-            "Created {0} -- {1}".format(rec.id, rec.title),
45
-        )
30
+        add_user_log(rec.id, rec.user_id, "albums", "info", "Created {0} -- {1}".format(rec.id, rec.title))
46 31
 
47 32
         flash(gettext("Created !"), "success")
48 33
     else:
49 34
         return render_template("album/new.jinja2", pcfg=pcfg, form=form)
50
-    return redirect(
51
-        url_for("bp_albums.show", username=current_user.name, setslug=rec.slug)
52
-    )
35
+    return redirect(url_for("bp_albums.show", username=current_user.name, setslug=rec.slug))
53 36
 
54 37
 
55 38
 @bp_albums.route("/user/<string:username>/sets/<string:setslug>", methods=["GET"])
@@ -83,14 +66,10 @@ def show(username, setslug):
83 66
     )
84 67
 
85 68
 
86
-@bp_albums.route(
87
-    "/user/<string:username>/sets/<string:setslug>/edit", methods=["GET", "POST"]
88
-)
69
+@bp_albums.route("/user/<string:username>/sets/<string:setslug>/edit", methods=["GET", "POST"])
89 70
 @login_required
90 71
 def edit(username, setslug):
91
-    album = Album.query.filter(
92
-        Album.user_id == current_user.id, Album.slug == setslug
93
-    ).first()
72
+    album = Album.query.filter(Album.user_id == current_user.id, Album.slug == setslug).first()
94 73
     if not album:
95 74
         flash(gettext("Album not found"), "error")
96 75
         return redirect(url_for("bp_users.profile", name=username))
@@ -99,7 +78,7 @@ def edit(username, setslug):
99 78
         flash(gettext("Forbidden"), "error")
100 79
         return redirect(url_for("bp_users.profile", name=username))
101 80
 
102
-    pcfg = {"title": gettext(u"Edit %(title)s", title=album.title)}
81
+    pcfg = {"title": gettext("Edit %(title)s", title=album.title)}
103 82
 
104 83
     form = AlbumForm(request.form, obj=album)
105 84
 
@@ -118,26 +97,15 @@ def edit(username, setslug):
118 97
             db.session.commit()
119 98
 
120 99
             # log
121
-            add_user_log(
122
-                album.id,
123
-                album.user.id,
124
-                "albums",
125
-                "info",
126
-                "Edited {0} -- {1}".format(album.id, album.title),
127
-            )
100
+            add_user_log(album.id, album.user.id, "albums", "info", "Edited {0} -- {1}".format(album.id, album.title))
128 101
 
129
-            return redirect(
130
-                url_for("bp_albums.show", username=username, setslug=album.slug)
131
-            )
102
+            return redirect(url_for("bp_albums.show", username=username, setslug=album.slug))
132 103
     else:
133 104
         flash(gettext("Public album cannot have private sounds"), "error")
134 105
     return render_template("album/edit.jinja2", pcfg=pcfg, form=form, album=album)
135 106
 
136 107
 
137
-@bp_albums.route(
138
-    "/user/<string:username>/sets/<string:setslug>/delete",
139
-    methods=["GET", "DELETE", "PUT"],
140
-)
108
+@bp_albums.route("/user/<string:username>/sets/<string:setslug>/delete", methods=["GET", "DELETE", "PUT"])
141 109
 def delete(username, setslug):
142 110
     user = User.query.filter(User.name == username).first()
143 111
     if not user:
@@ -157,20 +125,12 @@ def delete(username, setslug):
157 125
     db.session.commit()
158 126
 
159 127
     # log
160
-    add_user_log(
161
-        album.id,
162
-        user.id,
163
-        "albums",
164
-        "info",
165
-        "Deleted {0} -- {1}".format(album.id, album.title),
166
-    )
128
+    add_user_log(album.id, user.id, "albums", "info", "Deleted {0} -- {1}".format(album.id, album.title))
167 129
 
168 130
     return redirect(url_for("bp_users.profile", name=username))
169 131
 
170 132
 
171
-@bp_albums.route(
172
-    "/user/<string:username>/sets/<string:setslug>/reorder.json", methods=["POST"]
173
-)
133
+@bp_albums.route("/user/<string:username>/sets/<string:setslug>/reorder.json", methods=["POST"])
174 134
 def reorder_json(username, setslug):
175 135
     user = User.query.filter(User.name == username).first()
176 136
     if not user:
@@ -199,16 +159,13 @@ def reorder_json(username, setslug):
199 159
         raise InvalidUsage("Invalid json", status_code=500)
200 160
 
201 161
     for snd in request.get_json()["data"]:
202
-        sound = Sound.query.filter(
203
-            Sound.id == int(snd["soundid"]), Sound.album_id == album.id
204
-        ).first()
162
+        sound = Sound.query.filter(Sound.id == int(snd["soundid"]), Sound.album_id == album.id).first()
205 163
         if not sound:
206 164
             raise InvalidUsage("Sound not found", status_code=404)
207 165
 
208 166
         if sound.album_order != int(snd["oldPosition"]):
209 167
             raise InvalidUsage(
210
-                "Old position %s doesn't match bdd one %s"
211
-                % (int(snd["oldPosition"]), sound.album_order)
168
+                "Old position %s doesn't match bdd one %s" % (int(snd["oldPosition"]), sound.album_order)
212 169
             )
213 170
         sound.album_order = int(snd["newPosition"])
214 171
 

+ 11
- 50
controllers/api/v1/activitypub.py View File

@@ -1,15 +1,4 @@
1
-from flask import (
2
-    Blueprint,
3
-    request,
4
-    abort,
5
-    current_app,
6
-    Response,
7
-    jsonify,
8
-    flash,
9
-    render_template,
10
-    redirect,
11
-    url_for,
12
-)
1
+from flask import Blueprint, request, abort, current_app, Response, jsonify, flash, render_template, redirect, url_for
13 2
 from little_boxes import activitypub
14 3
 from little_boxes.httpsig import verify_request
15 4
 from activitypub.backend import post_to_inbox, Box
@@ -34,22 +23,15 @@ def user_inbox(name):
34 23
     current_app.logger.debug(f"raw_data={data}")
35 24
 
36 25
     try:
37
-        if not verify_request(
38
-            request.method, request.path, request.headers, request.data
39
-        ):
26
+        if not verify_request(request.method, request.path, request.headers, request.data):
40 27
             raise Exception("failed to verify request")
41 28
     except Exception:
42 29
         current_app.logger.exception("failed to verify request")
43 30
         try:
44 31
             data = be.fetch_iri(data["id"])
45 32
         except Exception:
46
-            current_app.logger.exception(
47
-                f"failed to fetch remote id " f"at {data['id']}"
48
-            )
49
-            resp = {
50
-                "error": "failed to verify request "
51
-                "(using HTTP signatures or fetching the IRI)"
52
-            }
33
+            current_app.logger.exception(f"failed to fetch remote id " f"at {data['id']}")
34
+            resp = {"error": "failed to verify request " "(using HTTP signatures or fetching the IRI)"}
53 35
             response = jsonify(resp)
54 36
             response.mimetype = "application/json; charset=utf-8"
55 37
             response.status = 422
@@ -87,13 +69,7 @@ def followings(name):
87 69
 
88 70
     followings = user.actor[0].followings
89 71
 
90
-    return render_template(
91
-        "users/followings.jinja2",
92
-        pcfg=pcfg,
93
-        user=user,
94
-        actor=user.actor[0],
95
-        followings=followings,
96
-    )
72
+    return render_template("users/followings.jinja2", pcfg=pcfg, user=user, actor=user.actor[0], followings=followings)
97 73
 
98 74
 
99 75
 @bp_ap.route("/user/<string:name>/followers", methods=["GET"])
@@ -107,11 +83,7 @@ def followers(name):
107 83
         return redirect(url_for("bp_main.home"))
108 84
 
109 85
     return render_template(
110
-        "users/followers.jinja2",
111
-        pcfg=pcfg,
112
-        user=user,
113
-        actor=user.actor[0],
114
-        followers=user.actor[0].followers,
86
+        "users/followers.jinja2", pcfg=pcfg, user=user, actor=user.actor[0], followers=user.actor[0].followers
115 87
     )
116 88
 
117 89
 
@@ -134,9 +106,7 @@ def user_followers(name):
134 106
     actor = user.actor[0]
135 107
     followers = actor.followers
136 108
 
137
-    return jsonify(
138
-        **build_ordered_collection(followers, actor.url, request.args.get("page"))
139
-    )
109
+    return jsonify(**build_ordered_collection(followers, actor.url, request.args.get("page")))
140 110
 
141 111
 
142 112
 @bp_ap.route("/inbox", methods=["GET", "POST"])
@@ -152,22 +122,15 @@ def inbox():
152 122
     current_app.logger.debug(f"raw_data={data}")
153 123
 
154 124
     try:
155
-        if not verify_request(
156
-            request.method, request.path, request.headers, request.data
157
-        ):
125
+        if not verify_request(request.method, request.path, request.headers, request.data):
158 126
             raise Exception("failed to verify request")
159 127
     except Exception:
160 128
         current_app.logger.exception("failed to verify request")
161 129
         try:
162 130
             data = be.fetch_iri(data["id"])
163 131
         except Exception:
164
-            current_app.logger.exception(
165
-                f"failed to fetch remote id " f"at {data['id']}"
166
-            )
167
-            resp = {
168
-                "error": "failed to verify request "
169
-                "(using HTTP signatures or fetching the IRI)"
170
-            }
132
+            current_app.logger.exception(f"failed to fetch remote id " f"at {data['id']}")
133
+            resp = {"error": "failed to verify request " "(using HTTP signatures or fetching the IRI)"}
171 134
             response = jsonify(resp)
172 135
             response.mimetype = "application/json; charset=utf-8"
173 136
             response.status = 422
@@ -206,9 +169,7 @@ def outbox_item(item_id):
206 169
 
207 170
     current_app.logger.debug(f"activity url {be.activity_url(item_id)}")
208 171
 
209
-    item = Activity.query.filter(
210
-        Activity.box == Box.OUTBOX.value, Activity.url == be.activity_url(item_id)
211
-    ).first()
172
+    item = Activity.query.filter(Activity.box == Box.OUTBOX.value, Activity.url == be.activity_url(item_id)).first()
212 173
     if not item:
213 174
         abort(404)
214 175
 

+ 1
- 3
controllers/api/v1/nodeinfo.py View File

@@ -9,9 +9,7 @@ bp_nodeinfo = Blueprint("bp_nodeinfo", __name__, url_prefix="/nodeinfo")
9 9
 def nodeinfo():
10 10
     _config = Config.query.one()
11 11
     if not _config:
12
-        return Response(
13
-            "", status=500, content_type="application/jrd+json; charset=utf-8"
14
-        )
12
+        return Response("", status=500, content_type="application/jrd+json; charset=utf-8")
15 13
 
16 14
     resp = {
17 15
         "version": "2.0",

+ 7
- 24
controllers/api/v1/well_known.py View File

@@ -8,36 +8,26 @@ bp_wellknown = Blueprint("bp_wellknown", __name__, url_prefix="/.well-known")
8 8
 def webfinger():
9 9
     resource = request.args.get("resource")
10 10
     if not resource:
11
-        return Response(
12
-            "", status=400, content_type="application/jrd+json; charset=utf-8"
13
-        )
11
+        return Response("", status=400, content_type="application/jrd+json; charset=utf-8")
14 12
 
15 13
     id_list = resource.split(":")
16 14
     if len(id_list) < 2:
17
-        return Response(
18
-            "", status=400, content_type="application/jrd+json; charset=utf-8"
19
-        )
15
+        return Response("", status=400, content_type="application/jrd+json; charset=utf-8")
20 16
 
21 17
     try:
22 18
         user_id, domain = id_list[1].split("@")
23 19
     except ValueError:
24
-        return Response(
25
-            "", status=400, content_type="application/jrd+json; charset=utf-8"
26
-        )
20
+        return Response("", status=400, content_type="application/jrd+json; charset=utf-8")
27 21
 
28 22
     if len(id_list) == 3:
29 23
         domain += f":{id_list[2]}"
30 24
 
31 25
     if not (domain == current_app.config["AP_DOMAIN"]):
32
-        return Response(
33
-            "", status=404, content_type="application/jrd+json; charset=utf-8"
34
-        )
26
+        return Response("", status=404, content_type="application/jrd+json; charset=utf-8")
35 27
 
36 28
     user = db.session.query(User).filter_by(name_insensitive=user_id).first()
37 29
     if not user:
38
-        return Response(
39
-            "", status=404, content_type="application/jrd+json; charset=utf-8"
40
-        )
30
+        return Response("", status=404, content_type="application/jrd+json; charset=utf-8")
41 31
 
42 32
     method = "https"
43 33
 
@@ -45,11 +35,7 @@ def webfinger():
45 35
         "subject": f"acct:{user.name}@{domain}",
46 36
         "aliases": [f"{method}://{domain}/user/{user.name}"],
47 37
         "links": [
48
-            {
49
-                "rel": "self",
50
-                "type": "application/activity+json",
51
-                "href": f"{method}://{domain}/user/{user.name}",
52
-            }
38
+            {"rel": "self", "type": "application/activity+json", "href": f"{method}://{domain}/user/{user.name}"}
53 39
         ],
54 40
     }
55 41
 
@@ -64,10 +50,7 @@ def nodeinfo():
64 50
     domain = current_app.config["AP_DOMAIN"]
65 51
     resp = {
66 52
         "links": [
67
-            {
68
-                "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
69
-                "href": f"{method}://{domain}/nodeinfo/2.0",
70
-            }
53
+            {"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0", "href": f"{method}://{domain}/nodeinfo/2.0"}
71 54
         ]
72 55
     }
73 56
     response = jsonify(resp)

+ 53
- 42
controllers/search.py View File

@@ -1,19 +1,12 @@
1
-from flask import (
2
-    Blueprint,
3
-    render_template,
4
-    request,
5
-    redirect,
6
-    url_for,
7
-    flash,
8
-    current_app,
9
-)
1
+from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app
10 2
 from flask_babelex import gettext
11 3
 
12
-from models import User
4
+from models import db, User, Follower, Actor
13 5
 from little_boxes.webfinger import get_actor_url
14 6
 from little_boxes.urlutils import InvalidURLError
15 7
 from little_boxes import activitypub as ap
16 8
 from urllib.parse import urlparse
9
+from flask_security import current_user
17 10
 
18 11
 bp_search = Blueprint("bp_search", __name__, url_prefix="/search")
19 12
 
@@ -23,42 +16,60 @@ def users():
23 16
     who = request.args.get("who")
24 17
     pcfg = {"title": gettext("Search user")}
25 18
 
26
-    # Search is to be done in two steps:
27
-    # 1. Search from local User
28
-    # 2. If not found, webfinger it
19
+    # No follow status for unauthenticated user search
20
+    if not current_user.is_authenticated:
21
+        local_users = User.query.filter(User.name.contains(who)).all()
29 22
 
30
-    local_users = User.query.filter(User.name.like(who)).all()
31
-
32
-    if len(local_users) > 0:
33
-        return render_template(
34
-            "search/local_user.jinja2", pcfg=pcfg, who=who, users=local_users
23
+        if len(local_users) > 0:
24
+            return render_template("search/local_users_unauth.jinja2", pcfg=pcfg, who=who, users=local_users)
25
+    else:
26
+        # (user, actor, follower) tuple
27
+        local_users = (
28
+            db.session.query(User, Actor, Follower)
29
+            .join(Actor, User.id == Actor.user_id)
30
+            .outerjoin(Follower, Actor.id == Follower.target_id)
31
+            .filter(User.name.contains(who))
32
+            .all()
35 33
         )
36 34
 
37
-    if not local_users:
38
-        try:
39
-            remote_actor_url = get_actor_url(who, debug=current_app.debug)
40
-        except InvalidURLError:
41
-            current_app.logger.exception(f"Invalid webfinger URL: {who}")
42
-            remote_actor_url = None
35
+        if len(local_users) > 0:
36
+            return render_template("search/local_users_auth.jinja2", pcfg=pcfg, who=who, users=local_users)
43 37
 
44
-        if not remote_actor_url:
45
-            flash(gettext("User not found"), "error")
46
-            return redirect(url_for("bp_main.home"))
38
+        if not local_users:
39
+            current_app.logger.debug(f"searching for {who}")
40
+            try:
41
+                remote_actor_url = get_actor_url(who, debug=current_app.debug)
42
+            except (InvalidURLError, ValueError):
43
+                current_app.logger.exception(f"Invalid webfinger URL: {who}")
44
+                remote_actor_url = None
47 45
 
48
-        # We need to get the remote Actor
49
-        backend = ap.get_backend()
50
-        iri = backend.fetch_iri(remote_actor_url)
51
-        if not iri:
52
-            flash(gettext("User not found"), "error")
53
-            return redirect(url_for("bp_main.home"))
46
+            if not remote_actor_url:
47
+                flash(gettext("User not found"), "error")
48
+                return redirect(url_for("bp_main.home"))
54 49
 
55
-        domain = urlparse(iri["url"])
56
-        user = {
57
-            "name": iri["preferredUsername"],
58
-            "instance": domain.netloc,
59
-            "url": iri["url"],
60
-        }
50
+            # We need to get the remote Actor
51
+            backend = ap.get_backend()
52
+            iri = backend.fetch_iri(remote_actor_url)
53
+            if not iri:
54
+                flash(gettext("User not found"), "error")
55
+                return redirect(url_for("bp_main.home"))
61 56
 
62
-        return render_template(
63
-            "search/remote_user.jinja2", pcfg=pcfg, who=who, user=user
64
-        )
57
+            current_app.logger.debug(f"got remote actor URL {remote_actor_url}")
58
+
59
+            follow_rel = (
60
+                db.session.query(Actor.id, Follower.id)
61
+                .outerjoin(Follower, Actor.id == Follower.target_id)
62
+                .filter(Actor.url == remote_actor_url)
63
+                .first()
64
+            )
65
+            follow_status = follow_rel[1] is not None
66
+
67
+            domain = urlparse(iri["url"])
68
+            user = {
69
+                "name": iri["preferredUsername"],
70
+                "instance": domain.netloc,
71
+                "url": iri["url"],
72
+                "follow": follow_status,
73
+            }
74
+
75
+            return render_template("search/remote_user.jinja2", pcfg=pcfg, who=who, user=user)

+ 18
- 74
controllers/sound.py View File

@@ -1,14 +1,4 @@
1
-from flask import (
2
-    Blueprint,
3
-    render_template,
4
-    request,
5
-    redirect,
6
-    url_for,
7
-    flash,
8
-    Response,
9
-    abort,
10
-    json,
11
-)
1
+from flask import Blueprint, render_template, request, redirect, url_for, flash, Response, abort, json
12 2
 from flask_babelex import gettext
13 3
 from flask_security import login_required, current_user
14 4
 from flask_uploads import UploadSet, AUDIO
@@ -29,14 +19,10 @@ def show(username, soundslug):
29 19
         flash(gettext("User not found"), "error")
30 20
         return redirect(url_for("bp_main.home"))
31 21
     if current_user.is_authenticated and user.id == current_user.id:
32
-        sound = Sound.query.filter(
33
-            Sound.slug == soundslug, Sound.user_id == user.id
34
-        ).first()
22
+        sound = Sound.query.filter(Sound.slug == soundslug, Sound.user_id == user.id).first()
35 23
     else:
36 24
         sound = Sound.query.filter(
37
-            Sound.slug == soundslug,
38
-            Sound.user_id == user.id,
39
-            Sound.transcode_state == Sound.TRANSCODE_DONE,
25
+            Sound.slug == soundslug, Sound.user_id == user.id, Sound.transcode_state == Sound.TRANSCODE_DONE
40 26
         ).first()
41 27
 
42 28
     if not sound:
@@ -63,21 +49,15 @@ def show(username, soundslug):
63 49
     # if si and si.type == "FLAC":
64 50
     #    flash(gettext("No HTML5 player supported actually"), 'info')
65 51
 
66
-    return render_template(
67
-        "sound/show.jinja2", pcfg=pcfg, user=user, sound=sound, waveform=si_w
68
-    )
52
+    return render_template("sound/show.jinja2", pcfg=pcfg, user=user, sound=sound, waveform=si_w)
69 53
 
70 54
 
71
-@bp_sound.route(
72
-    "/user/<string:username>/track/<string:soundslug>/waveform.json", methods=["GET"]
73
-)
55
+@bp_sound.route("/user/<string:username>/track/<string:soundslug>/waveform.json", methods=["GET"])
74 56
 def waveform_json(username, soundslug):
75 57
     user = User.query.filter(User.name == username).first()
76 58
     if not user:
77 59
         raise InvalidUsage("User not found", status_code=404)
78
-    sound = Sound.query.filter(
79
-        Sound.slug == soundslug, Sound.user_id == user.id
80
-    ).first()
60
+    sound = Sound.query.filter(Sound.slug == soundslug, Sound.user_id == user.id).first()
81 61
     if not sound:
82 62
         raise InvalidUsage("Sound not found", status_code=404)
83 63
 
@@ -131,10 +111,7 @@ def upload():
131 111
                 rec.title = form.title.data
132 112
             rec.private = form.private.data
133 113
 
134
-            if (
135
-                "flac" in request.files["sound"].mimetype
136
-                or "ogg" in request.files["sound"].mimetype
137
-            ):
114
+            if "flac" in request.files["sound"].mimetype or "ogg" in request.files["sound"].mimetype:
138 115
                 rec.transcode_state = Sound.TRANSCODE_WAITING
139 116
                 rec.transcode_needed = True
140 117
 
@@ -147,35 +124,21 @@ def upload():
147 124
             upload_workflow.send(rec.id)
148 125
 
149 126
             # log
150
-            add_user_log(
151
-                rec.id,
152
-                user.id,
153
-                "sounds",
154
-                "info",
155
-                "Uploaded {0} -- {1}".format(rec.id, rec.title),
156
-            )
127
+            add_user_log(rec.id, user.id, "sounds", "info", "Uploaded {0} -- {1}".format(rec.id, rec.title))
157 128
 
158 129
             flash(gettext("Uploaded ! Processing will now follow."), "success")
159 130
         else:
160
-            return render_template(
161
-                "sound/upload.jinja2", pcfg=pcfg, form=form, flash="Error with the file"
162
-            )
163
-        return redirect(
164
-            url_for("bp_sound.show", username=current_user.name, soundslug=rec.slug)
165
-        )
131
+            return render_template("sound/upload.jinja2", pcfg=pcfg, form=form, flash="Error with the file")
132
+        return redirect(url_for("bp_sound.show", username=current_user.name, soundslug=rec.slug))
166 133
 
167 134
     # GET
168 135
     return render_template("sound/upload.jinja2", pcfg=pcfg, form=form)
169 136
 
170 137
 
171
-@bp_sound.route(
172
-    "/user/<string:username>/track/<string:soundslug>/edit", methods=["GET", "POST"]
173
-)
138
+@bp_sound.route("/user/<string:username>/track/<string:soundslug>/edit", methods=["GET", "POST"])
174 139
 @login_required
175 140
 def edit(username, soundslug):
176
-    sound = Sound.query.filter(
177
-        Sound.user_id == current_user.id, Sound.slug == soundslug
178
-    ).first()
141
+    sound = Sound.query.filter(Sound.user_id == current_user.id, Sound.slug == soundslug).first()
179 142
     if not sound:
180 143
         flash(gettext("Sound not found"), "error")
181 144
         return redirect(url_for("bp_users.profile", name=username))
@@ -184,7 +147,7 @@ def edit(username, soundslug):
184 147
         flash(gettext("Forbidden"), "error")
185 148
         return redirect(url_for("bp_users.profile", name=username))
186 149
 
187
-    pcfg = {"title": gettext(u"Edit %(title)s", title=sound.title)}
150
+    pcfg = {"title": gettext("Edit %(title)s", title=sound.title)}
188 151
 
189 152
     form = SoundEditForm(request.form, obj=sound)
190 153
 
@@ -203,29 +166,16 @@ def edit(username, soundslug):
203 166
 
204 167
         db.session.commit()
205 168
         # log
206
-        add_user_log(
207
-            sound.id,
208
-            sound.user.id,
209
-            "sounds",
210
-            "info",
211
-            "Edited {0} -- {1}".format(sound.id, sound.title),
212
-        )
213
-        return redirect(
214
-            url_for("bp_sound.show", username=username, soundslug=sound.slug)
215
-        )
169
+        add_user_log(sound.id, sound.user.id, "sounds", "info", "Edited {0} -- {1}".format(sound.id, sound.title))
170
+        return redirect(url_for("bp_sound.show", username=username, soundslug=sound.slug))
216 171
 
217 172
     return render_template("sound/edit.jinja2", pcfg=pcfg, form=form, sound=sound)
218 173
 
219 174
 
220
-@bp_sound.route(
221
-    "/user/<string:username>/track/<string:soundslug>/delete",
222
-    methods=["GET", "DELETE", "PUT"],
223
-)
175
+@bp_sound.route("/user/<string:username>/track/<string:soundslug>/delete", methods=["GET", "DELETE", "PUT"])
224 176
 @login_required
225 177
 def delete(username, soundslug):
226
-    sound = Sound.query.filter(
227
-        Sound.user_id == current_user.id, Sound.slug == soundslug
228
-    ).first()
178
+    sound = Sound.query.filter(Sound.user_id == current_user.id, Sound.slug == soundslug).first()
229 179
     if not sound:
230 180
         flash(gettext("Sound not found"), "error")
231 181
         return redirect(url_for("bp_users.profile", name=username))
@@ -238,12 +188,6 @@ def delete(username, soundslug):
238 188
     db.session.commit()
239 189
 
240 190
     # log
241
-    add_user_log(
242
-        sound.id,
243
-        sound.user.id,
244
-        "sounds",
245
-        "info",
246
-        "Deleted {0} -- {1}".format(sound.id, sound.title),
247
-    )
191
+    add_user_log(sound.id, sound.user.id, "sounds", "info", "Deleted {0} -- {1}".format(sound.id, sound.title))
248 192
 
249 193
     return redirect(url_for("bp_users.profile", name=username))

+ 13
- 60
controllers/users.py View File

@@ -1,31 +1,10 @@
1 1
 import pytz
2
-from flask import (
3
-    Blueprint,
4
-    render_template,
5
-    request,
6
-    redirect,
7
-    url_for,
8
-    flash,
9
-    Response,
10
-    json,
11
-    jsonify,
12
-    current_app,
13
-)
2
+from flask import Blueprint, render_template, request, redirect, url_for, flash, Response, json, jsonify, current_app
14 3
 from flask_babelex import gettext
15 4
 from flask_security import login_required, current_user
16 5
 
17 6
 from forms import UserProfileForm
18
-from models import (
19
-    db,
20
-    User,
21
-    UserLogging,
22
-    Sound,
23
-    Album,
24
-    Follower,
25
-    Actor,
26
-    Activity,
27
-    create_remote_actor,
28
-)
7
+from models import db, User, UserLogging, Sound, Album, Follower, Actor, Activity, create_remote_actor
29 8
 from utils import add_user_log
30 9
 from flask_accept import accept_fallback
31 10
 from little_boxes.webfinger import get_actor_url
@@ -43,28 +22,19 @@ def logs():
43 22
     pcfg = {"title": gettext("User Logs")}
44 23
     if level:
45 24
         _logs = (
46
-            UserLogging.query.filter(
47
-                UserLogging.level == level.upper(),
48
-                UserLogging.user_id == current_user.id,
49
-            )
25
+            UserLogging.query.filter(UserLogging.level == level.upper(), UserLogging.user_id == current_user.id)
50 26
             .limit(100)
51 27
             .all()
52 28
         )
53 29
     else:
54
-        _logs = (
55
-            UserLogging.query.filter(UserLogging.user_id == current_user.id)
56
-            .limit(100)
57
-            .all()
58
-        )
30
+        _logs = UserLogging.query.filter(UserLogging.user_id == current_user.id).limit(100).all()
59 31
     return render_template("users/user_logs.jinja2", pcfg=pcfg, logs=_logs)
60 32
 
61 33
 
62 34
 @bp_users.route("/account/logs/<int:log_id>/delete", methods=["GET", "DELETE", "PUT"])
63 35
 @login_required
64 36
 def logs_delete(log_id):
65
-    log = UserLogging.query.filter(
66
-        UserLogging.id == log_id, UserLogging.user_id == current_user.id
67
-    ).first()
37
+    log = UserLogging.query.filter(UserLogging.id == log_id, UserLogging.user_id == current_user.id).first()
68 38
     if not log:
69 39
         _datas = {"status": "error", "id": log_id}
70 40
     else:
@@ -88,9 +58,7 @@ def profile(name):
88 58
         sounds = Sound.query.filter(Sound.user_id == user.id)
89 59
     else:
90 60
         sounds = Sound.query.filter(
91
-            Sound.user_id == user.id,
92
-            Sound.private.is_(False),
93
-            Sound.transcode_state == Sound.TRANSCODE_DONE,
61
+            Sound.user_id == user.id, Sound.private.is_(False), Sound.transcode_state == Sound.TRANSCODE_DONE
94 62
         )
95 63
 
96 64
     # FIXME better SQL for counting thoses relations
@@ -98,12 +66,7 @@ def profile(name):
98 66
     followers = len(user.actor[0].followers)
99 67
 
100 68
     return render_template(
101
-        "users/profile.jinja2",
102
-        pcfg=pcfg,
103
-        user=user,
104
-        sounds=sounds,
105
-        followings=followings,
106
-        followers=followers,
69
+        "users/profile.jinja2", pcfg=pcfg, user=user, sounds=sounds, followings=followings, followers=followers
107 70
     )
108 71
 
109 72
 
@@ -136,9 +99,7 @@ def profile_albums(name):
136 99
     else:
137 100
         albums = Album.query.filter(Album.user_id == user.id, Album.private.is_(False))
138 101
 
139
-    return render_template(
140
-        "users/profile_albums.jinja2", pcfg=pcfg, user=user, albums=albums
141
-    )
102
+    return render_template("users/profile_albums.jinja2", pcfg=pcfg, user=user, albums=albums)
142 103
 
143 104
 
144 105
 @bp_users.route("/account/edit", methods=["GET", "POST"])
@@ -183,7 +144,7 @@ def follow():
183 144
 
184 145
     if local_user:
185 146
         # Process local follow
186
-        actor_me.follow(local_user.actor[0])
147
+        actor_me.follow(None, local_user.actor[0])
187 148
         flash(gettext("Follow successful"), "success")
188 149
     else:
189 150
         # Might be a remote follow
@@ -275,26 +236,18 @@ def unfollow():
275 236
             return redirect(url_for("bp_users.profile", name=current_user.name))
276 237
 
277 238
         # 3. Fetch the Activity of the Follow
278
-        accept_activity = Activity.query.filter(
279
-            Activity.url == follow_relation.activity_url
280
-        ).first()
239
+        accept_activity = Activity.query.filter(Activity.url == follow_relation.activity_url).first()
281 240
         if not accept_activity:
282
-            current_app.logger.error(
283
-                f"cannot find accept activity {follow_relation.activity_url}"
284
-            )
241
+            current_app.logger.error(f"cannot find accept activity {follow_relation.activity_url}")
285 242
             flash(gettext("Whoops, something went wrong"))
286 243
             return redirect(url_for("bp_users.profile", name=current_user.name))
287 244
         # Then the Activity ID of the Accept will be the object id
288 245
         activity = ap.parse_activity(payload=accept_activity.payload)
289 246
 
290 247
         # Get the final activity (the Follow one)
291
-        follow_activity = Activity.query.filter(
292
-            Activity.url == activity.get_object_id()
293
-        ).first()
248
+        follow_activity = Activity.query.filter(Activity.url == activity.get_object_id()).first()
294 249
         if not follow_activity:
295
-            current_app.logger.error(
296
-                f"cannot find follow activity {activity.get_object_id()}"
297
-            )
250
+            current_app.logger.error(f"cannot find follow activity {activity.get_object_id()}")
298 251
             flash(gettext("Whoops, something went wrong"))
299 252
             return redirect(url_for("bp_users.profile", name=current_user.name))
300 253
 

+ 1
- 5
dbseed.py View File

@@ -41,11 +41,7 @@ def seed_users(db):
41 41
     db.session.add(role_adm)
42 42
 
43 43
     user_datastore.create_user(
44
-        email="dashie@sigpipe.me",
45
-        password="fluttershy",
46
-        name="toto",
47
-        timezone="UTC",
48
-        roles=[role_adm],
44
+        email="dashie@sigpipe.me", password="fluttershy", name="toto", timezone="UTC", roles=[role_adm]
49 45
     )
50 46
     db.session.commit()
51 47
     return

+ 2
- 6
forms.py View File

@@ -36,9 +36,7 @@ class ModelForm(BaseModelForm):
36 36
 
37 37
 
38 38
 class ExtendedRegisterForm(RegisterForm):
39
-    name = StringField(
40
-        "Username", [DataRequired(), Regexp(regex="^\w+$"), Length(max=150)]
41
-    )
39
+    name = StringField("Username", [DataRequired(), Regexp(regex="^\w+$"), Length(max=150)])
42 40
 
43 41
     def validate_name(form, field):
44 42
         if len(field.data) <= 0:
@@ -59,9 +57,7 @@ class UserProfileForm(ModelForm):
59 57
     firstname = StringField(gettext("Firstname"), [Length(max=32)])
60 58
     lastname = StringField(gettext("Lastname"), [Length(max=32)])
61 59
     timezone = SelectField(coerce=str, label=gettext("Timezone"), default="UTC")
62
-    locale = SelectField(
63
-        gettext("Locale"), default="en", choices=[["en", "English"], ["fr", "French"]]
64
-    )
60
+    locale = SelectField(gettext("Locale"), default="en", choices=[["en", "English"], ["fr", "French"]])
65 61
     submit = SubmitField(gettext("Update profile"))
66 62
 
67 63
 

+ 3
- 7
migrations/env.py View File

@@ -22,9 +22,7 @@ logger = logging.getLogger("alembic.env")
22 22
 # from myapp import mymodel
23 23
 # target_metadata = mymodel.Base.metadata
24 24
 
25
-config.set_main_option(
26
-    "sqlalchemy.url", current_app.config.get("SQLALCHEMY_DATABASE_URI")
27
-)
25
+config.set_main_option("sqlalchemy.url", current_app.config.get("SQLALCHEMY_DATABASE_URI"))
28 26
 target_metadata = current_app.extensions["migrate"].db.metadata
29 27
 
30 28
 
@@ -72,9 +70,7 @@ def run_migrations_online():
72 70
                 logger.info("No changes in schema detected.")
73 71
 
74 72
     engine = engine_from_config(
75
-        config.get_section(config.config_ini_section),
76
-        prefix="sqlalchemy.",
77
-        poolclass=pool.NullPool,
73
+        config.get_section(config.config_ini_section), prefix="sqlalchemy.", poolclass=pool.NullPool
78 74
     )
79 75
 
80 76
     connection = engine.connect()
@@ -83,7 +79,7 @@ def run_migrations_online():
83 79
         target_metadata=target_metadata,
84 80
         process_revision_directives=process_revision_directives,
85 81
         transaction_per_migration=True,
86
-        **current_app.extensions["migrate"].configure_args
82
+        **current_app.extensions["migrate"].configure_args,
87 83
     )
88 84
 
89 85
     try:

+ 2
- 6
migrations/versions/00_795db5a5e99b_.py View File

@@ -63,9 +63,7 @@ def upgrade():
63 63
         sa.Column("category", sa.String(length=255), nullable=False),
64 64
         sa.Column("level", sa.String(length=255), nullable=False),
65 65
         sa.Column("message", sa.Text(), nullable=False),
66
-        sa.Column(
67
-            "timestamp", sa.DateTime(), server_default=sa.text("now()"), nullable=True
68
-        ),
66
+        sa.Column("timestamp", sa.DateTime(), server_default=sa.text("now()"), nullable=True),
69 67
         sa.Column("user_id", sa.Integer(), nullable=True),
70 68
         sa.ForeignKeyConstraint(["user_id"], ["user.id"]),
71 69
         sa.PrimaryKeyConstraint("id"),
@@ -83,9 +81,7 @@ def upgrade():
83 81
         sa.Column("category", sa.String(length=255), nullable=False),
84 82
         sa.Column("level", sa.String(length=255), nullable=False),
85 83
         sa.Column("message", sa.Text(), nullable=False),
86
-        sa.Column(
87
-            "timestamp", sa.DateTime(), server_default=sa.text("now()"), nullable=True
88
-        ),
84
+        sa.Column("timestamp", sa.DateTime(), server_default=sa.text("now()"), nullable=True),
89 85
         sa.Column("user_id", sa.Integer(), nullable=False),
90 86
         sa.ForeignKeyConstraint(["user_id"], ["user.id"]),
91 87
         sa.PrimaryKeyConstraint("id"),

+ 1
- 3
migrations/versions/01_da3273ca0f0f_.py View File

@@ -19,9 +19,7 @@ def upgrade():
19 19
         "sound",
20 20
         sa.Column("id", sa.Integer(), nullable=False),
21 21
         sa.Column("title", sa.String(length=255), nullable=True),
22
-        sa.Column(
23
-            "uploaded", sa.DateTime(), server_default=sa.text("now()"), nullable=True
24
-        ),
22
+        sa.Column("uploaded", sa.DateTime(), server_default=sa.text("now()"), nullable=True),
25 23
         sa.Column("description", sa.UnicodeText(), nullable=True),
26 24
         sa.Column("public", sa.Boolean(), nullable=False),
27 25
         sa.Column("slug", sa.String(length=255), nullable=True),

+ 1
- 3
migrations/versions/06_14ecb77e9482_.py View File

@@ -16,9 +16,7 @@ from alembic import op  # noqa: E402
16 16
 
17 17
 def upgrade():
18 18
     op.add_column("sound_info", sa.Column("bitrate", sa.Integer(), nullable=True))
19
-    op.add_column(
20
-        "sound_info", sa.Column("bitrate_mode", sa.String(length=10), nullable=True)
21
-    )
19
+    op.add_column("sound_info", sa.Column("bitrate_mode", sa.String(length=10), nullable=True))
22 20
 
23 21
 
24 22
 def downgrade():

+ 1
- 3
migrations/versions/08_fc50eb4e9b34_.py View File

@@ -15,9 +15,7 @@ from alembic import op  # noqa: E402
15 15
 
16 16
 
17 17
 def upgrade():
18
-    op.add_column(
19
-        "sound_info", sa.Column("type_human", sa.String(length=20), nullable=True)
20
-    )
18
+    op.add_column("sound_info", sa.Column("type_human", sa.String(length=20), nullable=True))
21 19
 
22 20
 
23 21
 def downgrade():

+ 1
- 3
migrations/versions/09_aeb6c1345690_.py View File

@@ -15,9 +15,7 @@ from alembic import op  # noqa: E402
15 15
 
16 16
 
17 17
 def upgrade():
18
-    op.add_column(
19
-        "sound_info", sa.Column("waveform_error", sa.Boolean(), nullable=True)
20
-    )
18
+    op.add_column("sound_info", sa.Column("waveform_error", sa.Boolean(), nullable=True))
21 19
 
22 20
 
23 21
 def downgrade():

+ 1
- 3
migrations/versions/10_a901f35d5686_.py View File

@@ -15,9 +15,7 @@ from alembic import op  # noqa: E402
15 15
 
16 16
 
17 17
 def upgrade():
18
-    op.add_column(
19
-        "sound", sa.Column("filename_orig", sa.String(length=255), nullable=True)
20
-    )
18
+    op.add_column("sound", sa.Column("filename_orig", sa.String(length=255), nullable=True))
21 19
 
22 20
 
23 21
 def downgrade():

+ 1
- 3
migrations/versions/11_f41ac4617ef6_.py View File

@@ -20,7 +20,5 @@ def upgrade():
20 20
 
21 21
 
22 22
 def downgrade():
23
-    op.add_column(
24
-        "sound", sa.Column("public", sa.BOOLEAN(), autoincrement=False, nullable=False)
25
-    )
23
+    op.add_column("sound", sa.Column("public", sa.BOOLEAN(), autoincrement=False, nullable=False))
26 24
     op.drop_column("sound", "private")

+ 2
- 7
migrations/versions/17_01e6d591fe6f_.py View File

@@ -15,14 +15,9 @@ from alembic import op  # noqa: E402
15 15
 
16 16
 
17 17
 def upgrade():
18
-    op.add_column(
19
-        "sound", sa.Column("filename_transcoded", sa.String(length=255), nullable=True)
20
-    )
18
+    op.add_column("sound", sa.Column("filename_transcoded", sa.String(length=255), nullable=True))
21 19
     op.add_column("sound", sa.Column("transcode_needed", sa.Boolean(), nullable=True))
22
-    op.add_column(
23
-        "sound",
24
-        sa.Column("transcode_state", sa.Integer(), server_default="0", nullable=False),
25
-    )
20
+    op.add_column("sound", sa.Column("transcode_state", sa.Integer(), server_default="0", nullable=False))
26 21
 
27 22
 
28 23
 def downgrade():

+ 1
- 3
migrations/versions/19_98b863ae920c_.py View File

@@ -15,9 +15,7 @@ import sqlalchemy as sa  # noqa: E402
15 15
 
16 16
 
17 17
 def upgrade():
18
-    op.add_column(
19
-        "sound", sa.Column("licence", sa.Integer(), nullable=False, server_default="0")
20
-    )
18
+    op.add_column("sound", sa.Column("licence", sa.Integer(), nullable=False, server_default="0"))
21 19
 
22 20
 
23 21
 def downgrade():

+ 2
- 12
migrations/versions/20_691eaff10a88_.py View File

@@ -15,20 +15,10 @@ import sqlalchemy as sa  # noqa: E402
15 15
 
16 16
 
17 17
 def upgrade():
18
-    op.alter_column(
19
-        "sound",
20
-        "licence",
21
-        existing_type=sa.INTEGER(),
22
-        nullable=True,
23
-        existing_server_default=sa.text("0"),
24
-    )
18
+    op.alter_column("sound", "licence", existing_type=sa.INTEGER(), nullable=True, existing_server_default=sa.text("0"))
25 19
 
26 20
 
27 21
 def downgrade():
28 22
     op.alter_column(
29
-        "sound",
30
-        "licence",
31
-        existing_type=sa.INTEGER(),
32
-        nullable=False,
33
-        existing_server_default=sa.text("0"),
23
+        "sound", "licence", existing_type=sa.INTEGER(), nullable=False, existing_server_default=sa.text("0")
34 24
     )

+ 2
- 12
migrations/versions/21_abf095d9c9f3_.py View File

@@ -16,19 +16,9 @@ import sqlalchemy as sa  # noqa: E402
16 16
 
17 17
 def upgrade():
18 18
     op.alter_column(
19
-        "sound",
20
-        "licence",
21
-        existing_type=sa.INTEGER(),
22
-        nullable=False,
23
-        existing_server_default=sa.text("0"),
19
+        "sound", "licence", existing_type=sa.INTEGER(), nullable=False, existing_server_default=sa.text("0")
24 20
     )
25 21
 
26 22
 
27 23
 def downgrade():
28
-    op.alter_column(
29
-        "sound",
30
-        "licence",
31
-        existing_type=sa.INTEGER(),
32
-        nullable=True,
33
-        existing_server_default=sa.text("0"),
34
-    )
24
+    op.alter_column("sound", "licence", existing_type=sa.INTEGER(), nullable=True, existing_server_default=sa.text("0"))

+ 2
- 6
migrations/versions/22_835be4f73770_.py View File

@@ -15,12 +15,8 @@ from alembic import op  # noqa: E402
15 15
 
16 16
 
17 17
 def upgrade():
18
-    op.alter_column(
19
-        "user_logging", "sound_id", existing_type=sa.INTEGER(), nullable=True
20
-    )
18
+    op.alter_column("user_logging", "sound_id", existing_type=sa.INTEGER(), nullable=True)
21 19
 
22 20
 
23 21
 def downgrade():
24
-    op.alter_column(
25
-        "user_logging", "sound_id", existing_type=sa.INTEGER(), nullable=False
26
-    )
22
+    op.alter_column("user_logging", "sound_id", existing_type=sa.INTEGER(), nullable=False)

+ 2
- 7
migrations/versions/23_2ec399782f52_.py View File

@@ -21,11 +21,6 @@ def upgrade():
21 21
 
22 22
 
23 23
 def downgrade():
24
-    op.add_column(
25
-        "user_logging",
26
-        sa.Column("sound_id", sa.INTEGER(), autoincrement=False, nullable=True),
27
-    )
28
-    op.create_foreign_key(
29
-        "user_logging_sound_id_fkey", "user_logging", "sound", ["sound_id"], ["id"]
30
-    )
24
+    op.add_column("user_logging", sa.Column("sound_id", sa.INTEGER(), autoincrement=False, nullable=True))
25
+    op.create_foreign_key("user_logging_sound_id_fkey", "user_logging", "sound", ["sound_id"], ["id"])
31 26
     op.drop_column("user_logging", "item_id")

+ 2
- 6
migrations/versions/25_32f48de123f3_.py View File

@@ -25,9 +25,7 @@ def upgrade():
25 25
         sa.Column("inbox_url", sqlalchemy_utils.types.url.URLType(), nullable=True),
26 26
         sa.Column("following_url", sqlalchemy_utils.types.url.URLType(), nullable=True),
27 27
         sa.Column("followers_url", sqlalchemy_utils.types.url.URLType(), nullable=True),
28
-        sa.Column(
29
-            "shared_inbox_url", sqlalchemy_utils.types.url.URLType(), nullable=True
30
-        ),
28
+        sa.Column("shared_inbox_url", sqlalchemy_utils.types.url.URLType(), nullable=True),
31 29
         sa.Column(
32 30
             "type",
33 31
             sqlalchemy_utils.types.choice.ChoiceType(choices=ACTOR_TYPE_CHOICES),
@@ -46,9 +44,7 @@ def upgrade():
46 44
         sa.Column("user_id", sa.Integer(), nullable=True),
47 45
         sa.ForeignKeyConstraint(["user_id"], ["user.id"]),
48 46
         sa.PrimaryKeyConstraint("id"),
49
-        sa.UniqueConstraint(
50
-            "domain", "preferred_username", name="_domain_pref_username_uc"
51
-        ),
47
+        sa.UniqueConstraint("domain", "preferred_username", name="_domain_pref_username_uc"),
52 48
     )
53 49
     op.create_index(op.f("ix_actor_url"), "actor", ["url"], unique=True)
54 50
 

+ 2
- 10
migrations/versions/27_687d40646d63_.py View File

@@ -18,20 +18,12 @@ from sqlalchemy.dialects import postgresql  # noqa: E402
18 18
 def upgrade():
19 19
     print("This migration may fails if you don't already have the " "extension created")
20 20
     print("If you get an error about 'function uuid_generate_v4() " "does not exist'")
21
-    print(
22
-        "please run 'CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";' "
23
-        "from postgresql shell"
24
-    )
21
+    print("please run 'CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";' " "from postgresql shell")
25 22
     print("with a valid superuser on your database.")
26 23
     op.create_table(
27 24
         "follow",
28 25
         sa.Column("id", sa.Integer(), nullable=False),
29
-        sa.Column(
30
-            "uuid",
31
-            postgresql.UUID(as_uuid=True),
32
-            server_default=sa.text("uuid_generate_v4()"),
33
-            nullable=True,
34
-        ),
26
+        sa.Column("uuid", postgresql.UUID(as_uuid=True), server_default=sa.text("uuid_generate_v4()"), nullable=True),
35 27
         sa.Column("creation_date", sa.DateTime(), nullable=True),
36 28
         sa.Column("modification_date", sa.DateTime(), nullable=True),
37 29
         sa.PrimaryKeyConstraint("id"),

+ 5
- 26
migrations/versions/28_2e83f405e9a4_.py View File

@@ -21,12 +21,7 @@ def upgrade():
21 21
         "activity",
22 22
         sa.Column("id", sa.Integer(), nullable=False),
23 23
         sa.Column("actor", sa.Integer(), nullable=True),
24
-        sa.Column(
25
-            "uuid",
26
-            postgresql.UUID(as_uuid=True),
27
-            server_default=sa.text("uuid_generate_v4()"),
28
-            nullable=True,
29
-        ),
24
+        sa.Column("uuid", postgresql.UUID(as_uuid=True), server_default=sa.text("uuid_generate_v4()"), nullable=True),
30 25
         sa.Column("url", sqlalchemy_utils.types.url.URLType(), nullable=True),
31 26
         sa.Column("type", sa.String(length=100), nullable=True),
32 27
         sa.Column("payload", sqlalchemy_utils.types.json.JSONType(), nullable=True),
@@ -42,12 +37,7 @@ def upgrade():
42 37
     op.create_table(
43 38
         "followers",
44 39
         sa.Column("id", sa.Integer(), nullable=False),
45
-        sa.Column(
46
-            "uuid",
47
-            postgresql.UUID(as_uuid=True),
48
-            server_default=sa.text("uuid_generate_v4()"),
49
-            nullable=True,
50
-        ),
40
+        sa.Column("uuid", postgresql.UUID(as_uuid=True), server_default=sa.text("uuid_generate_v4()"), nullable=True),
51 41
         sa.Column("actor_id", sa.Integer(), nullable=True),
52 42
         sa.Column("target_id", sa.Integer(), nullable=True),
53 43
         sa.Column("creation_date", sa.DateTime(), nullable=True),
@@ -66,21 +56,10 @@ def downgrade():
66 56
         "follow",
67 57
         sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
68 58
         sa.Column(
69
-            "uuid",
70
-            postgresql.UUID(),
71
-            server_default=sa.text("uuid_generate_v4()"),
72
-            autoincrement=False,
73
-            nullable=True,
74
-        ),
75
-        sa.Column(
76
-            "creation_date", postgresql.TIMESTAMP(), autoincrement=False, nullable=True
77
-        ),
78
-        sa.Column(
79
-            "modification_date",
80
-            postgresql.TIMESTAMP(),
81
-            autoincrement=False,
82
-            nullable=True,
59
+            "uuid", postgresql.UUID(), server_default=sa.text("uuid_generate_v4()"), autoincrement=False, nullable=True
83 60
         ),
61
+        sa.Column("creation_date", postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
62
+        sa.Column("modification_date", postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
84 63
         sa.PrimaryKeyConstraint("id", name="follow_pkey"),
85 64
         sa.UniqueConstraint("uuid", name="follow_uuid_key"),
86 65
     )

+ 1
- 4
migrations/versions/31_7ce00beb5b0a_.py View File

@@ -16,10 +16,7 @@ import sqlalchemy_utils  # noqa: E402
16 16
 
17 17
 
18 18
 def upgrade():
19
-    op.add_column(
20
-        "followers",
21
-        sa.Column("activity_url", sqlalchemy_utils.types.url.URLType(), nullable=True),
22
-    )
19
+    op.add_column("followers", sa.Column("activity_url", sqlalchemy_utils.types.url.URLType(), nullable=True))
23 20
     op.create_unique_constraint(None, "followers", ["activity_url"])
24 21
 
25 22
 

+ 39
- 101
models.py View File

@@ -60,12 +60,8 @@ class Role(db.Model, RoleMixin):
60 60
 
61 61
 class User(db.Model, UserMixin):
62 62
     id = db.Column(db.Integer, primary_key=True)
63
-    email = db.Column(
64
-        db.String(255), unique=True, nullable=False, info={"label": "Email"}
65
-    )
66
-    name = db.Column(
67
-        db.String(255), unique=True, nullable=False, info={"label": "Username"}
68
-    )
63
+    email = db.Column(db.String(255), unique=True, nullable=False, info={"label": "Email"})
64
+    name = db.Column(db.String(255), unique=True, nullable=False, info={"label": "Username"})
69 65
     password = db.Column(db.String(255), nullable=False, info={"label": "Password"})
70 66
     active = db.Column(db.Boolean())
71 67
     confirmed_at = db.Column(db.DateTime())
@@ -75,25 +71,15 @@ class User(db.Model, UserMixin):
75 71
 
76 72
     locale = db.Column(db.String(5), default="en")
77 73
 
78
-    timezone = db.Column(
79
-        db.String(255), nullable=False, default="UTC"
80
-    )  # Managed and fed by pytz
74
+    timezone = db.Column(db.String(255), nullable=False, default="UTC")  # Managed and fed by pytz
81 75
 
82 76
     slug = db.Column(db.String(255), unique=True, nullable=True)
83 77
 
84
-    roles = db.relationship(
85
-        "Role", secondary=roles_users, backref=db.backref("users", lazy="dynamic")
86
-    )
87
-    apitokens = db.relationship(
88
-        "Apitoken", backref="user", lazy="dynamic", cascade="delete"
89
-    )
78
+    roles = db.relationship("Role", secondary=roles_users, backref=db.backref("users", lazy="dynamic"))
79
+    apitokens = db.relationship("Apitoken", backref="user", lazy="dynamic", cascade="delete")
90 80
 
91
-    user_loggings = db.relationship(
92
-        "UserLogging", backref="user", lazy="dynamic", cascade="delete"
93
-    )
94
-    loggings = db.relationship(
95
-        "Logging", backref="user", lazy="dynamic", cascade="delete"
96
-    )
81
+    user_loggings = db.relationship("UserLogging", backref="user", lazy="dynamic", cascade="delete")
82
+    loggings = db.relationship("Logging", backref="user", lazy="dynamic", cascade="delete")
97 83
 
98 84
     sounds = db.relationship("Sound", backref="user", lazy="dynamic", cascade="delete")
99 85
     albums = db.relationship("Album", backref="user", lazy="dynamic", cascade="delete")
@@ -116,6 +102,9 @@ class User(db.Model, UserMixin):
116 102
     def name_insensitive(cls):
117 103
         return CaseInsensitiveComparator(cls.name)
118 104
 
105
+    def __repr__(self):
106
+        return f"<User(id='{self.id}', name='{self.name}')>"
107
+
119 108
 
120 109
 event.listen(User.name, "set", User.generate_slug, retval=False)
121 110
 
@@ -123,12 +112,8 @@ event.listen(User.name, "set", User.generate_slug, retval=False)
123 112
 class Apitoken(db.Model):
124 113
     id = db.Column(db.Integer, primary_key=True)
125 114
     user_id = db.Column(db.Integer(), db.ForeignKey("user.id"), nullable=False)
126
-    token = db.Column(
127
-        db.String(255), unique=True, nullable=False, info={"label": "Token"}
128
-    )
129
-    secret = db.Column(
130
-        db.String(255), unique=True, nullable=False, info={"label": "Secret"}
131
-    )
115
+    token = db.Column(db.String(255), unique=True, nullable=False, info={"label": "Token"})
116
+    secret = db.Column(db.String(255), unique=True, nullable=False, info={"label": "Secret"})
132 117
 
133 118
 
134 119
 user_datastore = SQLAlchemyUserDatastore(db, User, Role)
@@ -144,9 +129,7 @@ class Logging(db.Model):
144 129
     category = db.Column(db.String(255), nullable=False, default="General")
145 130
     level = db.Column(db.String(255), nullable=False, default="INFO")
146 131
     message = db.Column(db.Text, nullable=False)
147
-    timestamp = db.Column(
148
-        db.DateTime(timezone=False), server_default=func.now(), onupdate=func.now()
149
-    )
132
+    timestamp = db.Column(db.DateTime(timezone=False), server_default=func.now(), onupdate=func.now())
150 133
 
151 134
     user_id = db.Column(db.Integer(), db.ForeignKey("user.id"), nullable=True)
152 135
 
@@ -160,9 +143,7 @@ class UserLogging(db.Model):
160 143
     category = db.Column(db.String(255), nullable=False, default="General")
161 144
     level = db.Column(db.String(255), nullable=False, default="INFO")
162 145
     message = db.Column(db.Text, nullable=False)
163
-    timestamp = db.Column(
164
-        db.DateTime(timezone=False), server_default=func.now(), onupdate=func.now()
165
-    )
146
+    timestamp = db.Column(db.DateTime(timezone=False), server_default=func.now(), onupdate=func.now())
166 147
     item_id = db.Column(db.Integer(), nullable=True)
167 148
 
168 149
     user_id = db.Column(db.Integer(), db.ForeignKey("user.id"), nullable=False)
@@ -206,9 +187,7 @@ class Sound(db.Model):
206 187
     id = db.Column(db.Integer, primary_key=True)
207 188
     title = db.Column(db.String(255), nullable=True)
208 189
     uploaded = db.Column(db.DateTime(timezone=False), default=func.now())
209
-    updated = db.Column(
210
-        db.DateTime(timezone=False), default=func.now(), onupdate=func.now()
211
-    )
190
+    updated = db.Column(db.DateTime(timezone=False), default=func.now(), onupdate=func.now())
212 191
     # TODO genre
213 192
     # TODO tags
214 193
     # TODO picture ?
@@ -228,9 +207,7 @@ class Sound(db.Model):
228 207
 
229 208
     user_id = db.Column(db.Integer(), db.ForeignKey("user.id"), nullable=False)
230 209
     album_id = db.Column(db.Integer(), db.ForeignKey("album.id"), nullable=True)
231
-    sound_infos = db.relationship(
232
-        "SoundInfo", backref="sound_info", lazy="dynamic", cascade="delete"
233
-    )
210
+    sound_infos = db.relationship("SoundInfo", backref="sound_info", lazy="dynamic", cascade="delete")
234 211
 
235 212
     timeline = db.relationship("Timeline", uselist=False, back_populates="sound")
236 213
 
@@ -245,11 +222,7 @@ class Sound(db.Model):
245 222
         return os.path.join(self.user.slug, "{0}.png".format(self.filename))
246 223
 
247 224
     def path_sound(self, orig=False):
248
-        if (
249
-            self.transcode_needed
250
-            and self.transcode_state == self.TRANSCODE_DONE
251
-            and not orig
252
-        ):
225
+        if self.transcode_needed and self.transcode_state == self.TRANSCODE_DONE and not orig:
253 226
             return os.path.join(self.user.slug, self.filename_transcoded)
254 227
         else:
255 228
             return os.path.join(self.user.slug, self.filename)
@@ -273,9 +246,7 @@ class Album(db.Model):
273 246
     title = db.Column(db.String(255), nullable=True)
274 247
     created = db.Column(db.DateTime(timezone=False), default=datetime.datetime.utcnow)
275 248
     updated = db.Column(
276
-        db.DateTime(timezone=False),
277
-        default=datetime.datetime.utcnow,
278
-        onupdate=datetime.datetime.utcnow,
249
+        db.DateTime(timezone=False), default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow
279 250
     )
280 251
     # TODO tags
281 252
     description = db.Column(db.UnicodeText(), nullable=True)
@@ -365,11 +336,7 @@ def make_sound_slug(mapper, connection, target):
365 336
             title = "{0} {1}".format(target.id, target.title)
366 337
 
367 338
     slug = slugify(title[:255])
368
-    connection.execute(
369
-        Sound.__table__.update()
370
-        .where(Sound.__table__.c.id == target.id)
371
-        .values(slug=slug)
372
-    )
339
+    connection.execute(Sound.__table__.update().where(Sound.__table__.c.id == target.id).values(slug=slug))
373 340
 
374 341
 
375 342
 @event.listens_for(Album, "after_update")
@@ -377,11 +344,7 @@ def make_sound_slug(mapper, connection, target):
377 344
 def make_album_slug(mapper, connection, target):
378 345
     title = "{0} {1}".format(target.id, target.title)
379 346
     slug = slugify(title[:255])
380
-    connection.execute(
381
-        Album.__table__.update()
382
-        .where(Album.__table__.c.id == target.id)
383
-        .values(slug=slug)
384
-    )
347
+    connection.execute(Album.__table__.update().where(Album.__table__.c.id == target.id).values(slug=slug))
385 348
 
386 349
 
387 350
 # #### Federation ####
@@ -402,20 +365,17 @@ class Follower(db.Model):
402 365
     __tablename__ = "followers"
403 366
 
404 367
     id = db.Column(db.Integer, primary_key=True)
405
-    uuid = db.Column(
406
-        UUID(as_uuid=True), server_default=sa_text("uuid_generate_v4()"), unique=True
407
-    )
368
+    uuid = db.Column(UUID(as_uuid=True), server_default=sa_text("uuid_generate_v4()"), unique=True)
408 369
     actor_id = db.Column(db.Integer, db.ForeignKey("actor.id"))
409 370
     target_id = db.Column(db.Integer, db.ForeignKey("actor.id"))
410 371
     activity_url = db.Column(URLType(), unique=True, nullable=True)
411 372
     creation_date = db.Column(db.DateTime(timezone=False), default=func.now())
412
-    modification_date = db.Column(
413
-        db.DateTime(timezone=False), onupdate=datetime.datetime.now
414
-    )
373
+    modification_date = db.Column(db.DateTime(timezone=False), onupdate=datetime.datetime.now)
415 374
 
416
-    __table_args__ = (
417
-        UniqueConstraint("actor_id", "target_id", name="unique_following"),
418
-    )
375
+    __table_args__ = (UniqueConstraint("actor_id", "target_id", name="unique_following"),)
376
+
377
+    def __repr__(self):
378
+        return f"<Follower(id='{self.id}', actor_id='{self.actor_id}', target_id='{self.target_id}')>"
419 379
 
420 380
 
421 381
 class Actor(db.Model):
@@ -439,17 +399,11 @@ class Actor(db.Model):
439 399
     private_key = db.Column(db.String(5000), nullable=True)
440 400
     creation_date = db.Column(db.DateTime(timezone=False), default=func.now())
441 401
     last_fetch_date = db.Column(db.DateTime(timezone=False), default=func.now())
442
-    manually_approves_followers = db.Column(
443
-        db.Boolean, nullable=True, server_default=None
444
-    )
402
+    manually_approves_followers = db.Column(db.Boolean, nullable=True, server_default=None)
445 403
     # Who follows self
446
-    followers = db.relationship(
447
-        "Follower", backref="followers", primaryjoin=id == Follower.target_id
448
-    )
404
+    followers = db.relationship("Follower", backref="followers", primaryjoin=id == Follower.target_id)
449 405
     # Who self follows
450
-    followings = db.relationship(
451
-        "Follower", backref="followings", primaryjoin=id == Follower.actor_id
452
-    )
406
+    followings = db.relationship("Follower", backref="followings", primaryjoin=id == Follower.actor_id)
453 407
     # Relation on itself, intermediary with actor and target
454 408
     # By using an Association Object, which isn't possible except by using
455 409
     # two relations. This may be better than only one, and some hackish things
@@ -458,11 +412,10 @@ class Actor(db.Model):
458 412
     user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
459 413
     user = db.relationship("User", backref=db.backref("actor"))
460 414
 
461
-    __table_args__ = (
462
-        UniqueConstraint(
463
-            "domain", "preferred_username", name="_domain_pref_username_uc"
464
-        ),
465
-    )
415
+    __table_args__ = (UniqueConstraint("domain", "preferred_username", name="_domain_pref_username_uc"),)
416
+
417
+    def __repr__(self):
418
+        return f"<Actor(id='{self.id}', user_id='{self.user_id}', preferredUsername='{self.preferred_username}', domain='{self.domain}')>"
466 419
 
467 420
     def webfinger_subject(self):
468 421
         return f"{self.preferred_username}@{self.domain}"
@@ -479,9 +432,7 @@ class Actor(db.Model):
479 432
     def follow(self, activity_url, target):
480 433
         current_app.logger.debug(f"saving: {self.id} following {target.id}")
481 434
 
482
-        rel = Follower.query.filter(
483
-            Follower.actor_id == self.id, Follower.target_id == target.id
484
-        ).first()
435
+        rel = Follower.query.filter(Follower.actor_id == self.id, Follower.target_id == target.id).first()
485 436
 
486 437
         if not rel:
487 438
             rel = Follower()
@@ -494,30 +445,21 @@ class Actor(db.Model):
494 445
     def unfollow(self, target):
495 446
         current_app.logger.debug(f"saving: {self.id} unfollowing {target.id}")
496 447
 
497
-        rel = Follower.query.filter(
498
-            Follower.actor_id == self.id, Follower.target_id == target.id
499
-        ).first()
448
+        rel = Follower.query.filter(Follower.actor_id == self.id, Follower.target_id == target.id).first()
500 449
         if rel:
501 450
             db.session.delete(rel)
502 451
             db.session.commit()
503 452
 
504 453
     def to_dict(self):
505 454
         return {
506
-            "@context": [
507
-                "https://www.w3.org/ns/activitystreams",
508
-                "https://w3id.org/security/v1",
509
-            ],
455
+            "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
510 456
             "id": self.url,
511 457
             "type": self.type.code,
512 458
             "preferredUsername": self.preferred_username,
513 459
             "inbox": self.inbox_url,
514 460
             "outbox": self.outbox_url,
515 461
             "manuallyApprovesFollowers": self.manually_approves_followers,
516
-            "publicKey": {
517
-                "id": self.private_key_id(),
518
-                "owner": self.url,
519
-                "publicKeyPem": self.public_key,
520
-            },
462
+            "publicKey": {"id": self.private_key_id(), "owner": self.url, "publicKeyPem": self.public_key},
521 463
             "endpoints": {"sharedInbox": self.shared_inbox_url},
522 464
         }
523 465
 
@@ -528,9 +470,7 @@ class Activity(db.Model):
528 470
 
529 471
     actor = db.Column(db.Integer, db.ForeignKey("actor.id"))
530 472
 
531
-    uuid = db.Column(
532
-        UUID(as_uuid=True), server_default=sa_text("uuid_generate_v4()"), unique=True
533
-    )
473
+    uuid = db.Column(UUID(as_uuid=True), server_default=sa_text("uuid_generate_v4()"), unique=True)
534 474
     url = db.Column(URLType(), unique=True, nullable=True)
535 475
     type = db.Column(db.String(100), index=True)
536 476
     box = db.Column(db.String(100))
@@ -581,9 +521,7 @@ def create_remote_actor(activity_actor: ap.BaseActivity):
581 521
     actor.name = activity_actor.preferredUsername  # mastodon don't have .name
582 522
     actor.manually_approves_followers = False
583 523
     actor.url = activity_actor.id  # FIXME: or .id ??? [cf backend.py:52-53]
584
-    actor.shared_inbox_url = activity_actor._data.get("endpoints", {}).get(
585
-        "sharedInbox"
586
-    )
524
+    actor.shared_inbox_url = activity_actor._data.get("endpoints", {}).get("sharedInbox")
587 525
     actor.inbox_url = activity_actor.inbox
588 526
     actor.outbox_url = activity_actor.outbot
589 527
     actor.public_key = activity_actor.get_key().pubkey_pem

+ 2
- 0
pyproject.toml View File

@@ -1,4 +1,6 @@
1 1
 [tool.black]
2
+line-length = 120
3
+py36 = true
2 4
 exclude = '''
3 5
 /(
4 6
     \.git

+ 1
- 0
setup.cfg View File

@@ -7,3 +7,4 @@ python_files = tests/*.py
7 7
 
8 8
 [flake8]
9 9
 max-line-length = 120
10
+ignore = E501

+ 25
- 0
templates/search/local_users_auth.jinja2 View File

@@ -0,0 +1,25 @@
1
+{% extends "layout.jinja2" %}
2
+
3
+{% block content %}
4
+    <div class="row">
5
+        <div class="col-lg-10"><h3>{{ gettext("Searching for: <i>%(username)s</i>", username=who) }}</h3></div>
6
+    </div>
7
+
8
+    <br/>
9
+
10
+    <div class="row">
11
+        <div class="col-lg-10">
12
+            {% for user in users %}
13
+                <div class="row_user">
14
+                    {{ gettext("Local user:") }} <a href="{{ url_for('bp_users.profile', name=user[0].name) }}">{{ user[0].name }}</a> -
15
+                    {% if user[2] %}
16
+                        <a href="{{ url_for("bp_users.unfollow", user=user[0].name) }}">unfollow</a>
17
+                    {% else %}
18
+                        <a href="{{ url_for("bp_users.follow", user=user[0].name) }}">follow</a>
19
+                    {% endif %}
20
+                </div>
21
+            {% endfor %}
22
+        </div>
23
+    </div>
24
+
25
+{% endblock %}

templates/search/local_user.jinja2 → templates/search/local_users_unauth.jinja2 View File


+ 6
- 1
templates/search/remote_user.jinja2 View File

@@ -10,7 +10,12 @@
10 10
     <div class="row">
11 11
         <div class="col-lg-10">
12 12
             <div class="row_user">
13
-                {{ gettext("Remote user:") }} <a href="{{ user['url'] }}">{{ user['name'] }}@{{ user['instance'] }}</a> - <a href="{{ url_for("bp_users.follow", user=user['url']) }}">follow</a> - <a href="{{ url_for("bp_users.unfollow", user=user['url']) }}">unfollow</a>
13
+                {{ gettext("Remote user:") }} <a href="{{ user['url'] }}">{{ user['name'] }}@{{ user['instance'] }}</a> -
14
+                {% if user.follow %}
15
+                    <a href="{{ url_for("bp_users.unfollow", user=user['url']) }}">unfollow</a>
16
+                {% else %}
17
+                    <a href="{{ url_for("bp_users.follow", user=user['url']) }}">follow</a>
18
+                {% endif %}
14 19
             </div>
15 20
         </div>
16 21
     </div>

+ 1
- 3
tests/helpers.py View File

@@ -4,9 +4,7 @@ from jsonschema import validate
4 4
 
5 5
 
6 6
 def login(client, email, password):
7
-    return client.post(
8
-        "/login", data=dict(email=email, password=password), follow_redirects=True
9
-    )
7
+    return client.post("/login", data=dict(email=email, password=password), follow_redirects=True)
10 8
 
11 9
 
12 10
 def logout(client):

+ 3
- 13
tests/test_c_users.py View File

@@ -34,19 +34,13 @@ def test_register_two_identical_users(client, session):
34 34
     resp = client.post(
35 35
         "/register",
36 36
         data=dict(
37
-            email="dashie+imunique@sigpipe.me",
38
-            password="fluttershy",
39
-            password_confirm="fluttershy",
40
-            name="ImUnique",
37
+            email="dashie+imunique@sigpipe.me", password="fluttershy", password_confirm="fluttershy", name="ImUnique"
41 38
         ),
42 39
         follow_redirects=True,
43 40
     )
44 41
     # should error
45 42
     assert b"Logged as" not in resp.data
46
-    assert (
47
-        b"dashie+imunique@sigpipe.me is already associated "
48
-        b"with an account." in resp.data
49
-    )
43
+    assert b"dashie+imunique@sigpipe.me is already associated " b"with an account." in resp.data
50 44
     assert b"Username already taken" in resp.data
51 45
     assert resp.status_code == 200
52 46
 
@@ -64,11 +58,7 @@ def test_change_password(client, session):
64 58
     # Change password
65 59
     resp = client.post(
66 60
         "/change",
67
-        data=dict(
68
-            password=init_password,
69
-            new_password=new_password,
70
-            new_password_confirm=new_password,
71
-        ),
61
+        data=dict(password=init_password, new_password=new_password, new_password_confirm=new_password),
72 62
         follow_redirects=True,
73 63
     )
74 64
 

+ 6
- 21
tests/test_wellknown.py View File

@@ -14,21 +14,14 @@ def test_webfinger(client, session):
14 14
     datas = rv.json
15 15
 
16 16
     assert "aliases" in datas
17
-    assert (
18
-        f"https://{current_app.config['AP_DOMAIN']}/user/TestWebfinger"
19
-        in datas["aliases"]
20
-    )
17
+    assert f"https://{current_app.config['AP_DOMAIN']}/user/TestWebfinger" in datas["aliases"]
21 18
     assert "links" in datas
22 19
     assert "subject" in datas
23
-    assert (
24
-        datas["subject"] == f"acct:TestWebfinger@" f"{current_app.config['AP_DOMAIN']}"
25
-    )
20
+    assert datas["subject"] == f"acct:TestWebfinger@" f"{current_app.config['AP_DOMAIN']}"
26 21
 
27 22
 
28 23
 def test_webfinger_case(client, session):
29
-    register(
30
-        client, "dashie+webfingercase@sigpipe.me", "fluttershy", "TestWebfingerCase"
31
-    )
24
+    register(client, "dashie+webfingercase@sigpipe.me", "fluttershy", "TestWebfingerCase")
32 25
     logout(client)
33 26
 
34 27
     rv = client.get("/.well-known/webfinger?resource=acct:testwebfingercase@localhost")
@@ -39,22 +32,14 @@ def test_webfinger_case(client, session):
39 32
     datas = rv.json
40 33
 
41 34
     assert "aliases" in datas
42
-    assert (
43
-        f"https://{current_app.config['AP_DOMAIN']}"
44
-        f"/user/TestWebfingerCase" in datas["aliases"]
45
-    )
35
+    assert f"https://{current_app.config['AP_DOMAIN']}" f"/user/TestWebfingerCase" in datas["aliases"]
46 36
     assert "links" in datas
47 37
     assert "subject" in datas
48
-    assert (
49
-        datas["subject"] == f"acct:TestWebfingerCase@"
50
-        f"{current_app.config['AP_DOMAIN']}"
51
-    )
38
+    assert datas["subject"] == f"acct:TestWebfingerCase@" f"{current_app.config['AP_DOMAIN']}"
52 39
 
53 40
 
54 41
 def test_unknown_webfinger(client, session):
55
-    rv = client.get(
56
-        "/.well-known/webfinger?resource=acct:TestWebfinger83294289@localhost"
57
-    )
42
+    rv = client.get("/.well-known/webfinger?resource=acct:TestWebfinger83294289@localhost")
58 43
     assert rv.headers["Content-Type"] == "application/jrd+json; charset=utf-8"
59 44
     assert rv.status_code == 404
60 45
 

+ 7
- 48
utils.py View File

@@ -61,18 +61,8 @@ def add_log(category, level, message):
61 61
 def add_user_log(item, user, category, level, message):
62 62
     if not category or not level or not message or not item:
63 63
         print("!! Fatal error in add_user_log() one of three variables not set")
64
-    print(
65
-        "[LOG][{0}][{1}][u:{2}i:{3}] {4}".format(
66
-            level.upper(), category, user, item, message
67
-        )
68
-    )
69
-    a = UserLogging(
70
-        category=category,
71
-        level=level.upper(),
72
-        message=message,
73
-        item_id=item,
74
-        user_id=user,
75
-    )
64
+    print("[LOG][{0}][{1}][u:{2}i:{3}] {4}".format(level.upper(), category, user, item, message))
65
+    a = UserLogging(category=category, level=level.upper(), message=message, item_id=item, user_id=user)
76 66
     db.session.add(a)
77 67
     db.session.commit()
78 68
 
@@ -132,26 +122,12 @@ def duration_song_human(seconds):
132 122
 def get_waveform(filename):
133 123
     binary = current_app.config["AUDIOWAVEFORM_BIN"]
134 124
     if not os.path.exists(binary) or not os.path.exists(filename):
135
-        add_log(
136
-            "AUDIOWAVEFORM",
137
-            "ERROR",
138
-            "Filename {0} or binary {1} invalid".format(filename, binary),
139
-        )
125
+        add_log("AUDIOWAVEFORM", "ERROR", "Filename {0} or binary {1} invalid".format(filename, binary))
140 126
         return None
141 127
 
142 128
     tmpjson = "{0}.json".format(filename)
143 129
 
144
-    cmd = [
145
-        binary,
146
-        "-i",
147
-        filename,
148
-        "--pixels-per-second",
149
-        "10",
150
-        "-b",
151
-        "8",
152
-        "-o",
153
-        tmpjson,
154
-    ]
130
+    cmd = [binary, "-i", filename, "--pixels-per-second", "10", "-b", "8", "-o", tmpjson]
155 131
 
156 132
     """
157 133
     Failed: Can't generate "xxx" from "xxx"
@@ -200,26 +176,11 @@ def get_waveform(filename):
200 176
 def create_png_waveform(fn_audio, fn_png):
201 177
     binary = current_app.config["AUDIOWAVEFORM_BIN"]
202 178
     if not os.path.exists(binary) or not os.path.exists(fn_audio):
203
-        add_log(
204
-            "AUDIOWAVEFORM_PNG",
205
-            "ERROR",
206
-            "Filename {0} or binary {1} invalid".format(fn_audio, binary),
207
-        )
179
+        add_log("AUDIOWAVEFORM_PNG", "ERROR", "Filename {0} or binary {1} invalid".format(fn_audio, binary))
208 180
         return None
209 181
 
210 182
     pngwf = "{0}.png".format(fn_png)
211
-    cmd = [
212
-        binary,
213
-        "-i",
214
-        fn_audio,
215
-        "--width",
216
-        "384",
217
-        "--height",
218
-        "64",
219
-        "--no-axis-labels",
220
-        "-o",
221
-        pngwf,
222
-    ]
183
+    cmd = [binary, "-i", fn_audio, "--width", "384", "--height", "64", "--no-axis-labels", "-o", pngwf]
223 184
 
224 185
     try:
225 186
         process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@@ -233,9 +194,7 @@ def create_png_waveform(fn_audio, fn_png):
233 194
     print("- Command ran with: {0}".format(process.args))
234 195
 
235 196
     if process.stderr.startswith(b"Can't generate"):
236
-        add_log(
237
-            "AUDIOWAVEFORM_PNG", "ERROR", "Process error: {0}".format(process.stderr)
238
-        )
197
+        add_log("AUDIOWAVEFORM_PNG", "ERROR", "Process error: {0}".format(process.stderr))
239 198
         return None
240 199
 
241 200
     return True

+ 8
- 27
workers.py View File

@@ -23,9 +23,7 @@ app = create_app()
23 23
 ctx = app.app_context()
24 24
 
25 25
 redis_broker = RedisBroker(
26
-    host=app.config["BROKER_REDIS_HOST"],
27
-    port=app.config["BROKER_REDIS_PORT"],
28
-    db=app.config["BROKER_REDIS_DB"],
26
+    host=app.config["BROKER_REDIS_HOST"], port=app.config["BROKER_REDIS_PORT"], db=app.config["BROKER_REDIS_DB"]
29 27
 )
30 28
 dramatiq.set_broker(redis_broker)
31 29
 
@@ -141,16 +139,10 @@ def work_transcode(sound_id):
141 139
 
142 140
     print("File: {0}: {1}".format(sound.id, sound.title))
143 141
     add_user_log(
144
-        sound.id,
145
-        sound.user.id,
146
-        "sounds",
147
-        "info",
148
-        "Transcoding started for: {0} -- {1}".format(sound.id, sound.title),
142
+        sound.id, sound.user.id, "sounds", "info", "Transcoding started for: {0} -- {1}".format(sound.id, sound.title)
149 143
     )
150 144
 
151
-    fname = os.path.join(
152
-        app.config["UPLOADED_SOUNDS_DEST"], sound.user.slug, sound.filename
153
-    )
145
+    fname = os.path.join(app.config["UPLOADED_SOUNDS_DEST"], sound.user.slug, sound.filename)
154 146
     _file, _ext = splitext(fname)
155 147
 
156 148
     _start = time.time()
@@ -173,11 +165,7 @@ def work_transcode(sound_id):
173 165
     db.session.commit()
174 166
 
175 167
     add_user_log(
176
-        sound.id,
177
-        sound.user.id,
178
-        "sounds",
179
-        "info",
180
-        "Transcoding finished for: {0} -- {1}".format(sound.id, sound.title),
168
+        sound.id, sound.user.id, "sounds", "info", "Transcoding finished for: {0} -- {1}".format(sound.id, sound.title)
181 169
     )
182 170
 
183 171
 
@@ -203,9 +191,7 @@ def work_metadatas(sound_id, force=False):
203 191
 
204 192
     # Generate Basic infos
205 193
 
206
-    fname = os.path.join(
207
-        app.config["UPLOADED_SOUNDS_DEST"], sound.user.slug, sound.filename
208
-    )
194
+    fname = os.path.join(app.config["UPLOADED_SOUNDS_DEST"], sound.user.slug, sound.filename)
209 195
 
210 196
     if not _infos.done_basic or force:
211 197
         print("- WORKING BASIC on {0}, {1}".format(sound.id, sound.filename))
@@ -241,13 +227,10 @@ def work_metadatas(sound_id, force=False):
241 227
                 sound.user.id,
242 228
                 "sounds",
243 229
                 "info",
244
-                "Got an error when generating waveform"
245
-                " for: {0} -- {1}".format(sound.id, sound.title),
230
+                "Got an error when generating waveform" " for: {0} -- {1}".format(sound.id, sound.title),
246 231
             )
247 232
         else:
248
-            fdir_wf = os.path.join(
249
-                app.config["UPLOADS_DEFAULT_DEST"], "waveforms", sound.user.slug
250
-            )
233
+            fdir_wf = os.path.join(app.config["UPLOADS_DEFAULT_DEST"], "waveforms", sound.user.slug)
251 234
             fname_wf = os.path.join(fdir_wf, sound.filename)
252 235
 
253 236
             if not os.path.isdir(fdir_wf):
@@ -287,9 +270,7 @@ def upload_workflow(sound_id):
287 270
         print("TRANSCODE finished")
288 271
 
289 272
         msg = Message(
290
-            subject="Song processing finished",
291
-            recipients=[sound.user.email],
292
-            sender=app.config["MAIL_DEFAULT_SENDER"],
273
+            subject="Song processing finished", recipients=[sound.user.email], sender=app.config["MAIL_DEFAULT_SENDER"]
293 274
         )
294 275
         msg.body = render_template("email/song_processed.txt", sound=sound)
295 276
         msg.html = render_template("email/song_processed.html", sound=sound)

Loading…
Cancel
Save