def unprocessed(): """ User unprocessed tracks timeline. --- tags: - Timelines parameters: - name: count in: query type: integer required: true description: count - name: page in: query type: integer description: page number responses: 200: description: Returns array of Status """ user = current_token.user if not user: return jsonify({"error": "Unauthorized"}), 403 count = int(request.args.get("count", 20)) page = int(request.args.get("page", 1)) q = Sound.query.filter( Sound.user_id == user.id, Sound.transcode_state.in_( (Sound.TRANSCODE_WAITING, Sound.TRANSCODE_PROCESSING, Sound.TRANSCODE_ERROR)), ) q = q.order_by(Sound.uploaded.desc()) q = q.paginate(page=page, per_page=count) tracks = [] for t in q.items: relationship = to_json_relationship(current_token.user, t.user) account = to_json_account(t.user, relationship) tracks.append(to_json_track(t, account)) resp = { "page": page, "page_size": count, "totalItems": q.total, "items": tracks, "totalPages": q.pages } return jsonify(resp)
def edit(username, soundslug): """ Edit track. --- tags: - Tracks security: - OAuth2: - write parameters: - name: username in: path type: string required: true description: User username - name: soundslug in: path type: string required: true description: Track slug responses: 200: description: Returns a Status with extra reel2bits params. """ current_user = current_token.user if not current_user: return jsonify({"error": "Unauthorized"}), 403 # Get the track sound = Sound.query.filter(Sound.user_id == current_user.id, Sound.slug == soundslug).first() if not sound: return jsonify({"error": "Not found"}), 404 album = request.json.get("album") description = request.json.get("description") licence = request.json.get("licence") private = request.json.get("private") title = request.json.get("title") genre = request.json.get("genre") tags = request.json.get("tags") if sound.private and not private: return jsonify({"error": "Cannot change to private: track already federated"}) if not title: title, _ = splitext(sound.filename_orig) else: sound.title = title sound.description = description sound.licence = licence sound.genre = genre # First remove tags which have been removed for tag in sound.tags: if tag.name not in tags: sound.tags.remove(tag) # Then add the new ones if new for tag in tags: if tag not in [a.name for a in sound.tags]: dbt = SoundTag.query.filter(SoundTag.name == tag).first() if not dbt: dbt = SoundTag(name=tag) db.session.add(dbt) sound.tags.append(dbt) # Purge orphaned tags for otag in SoundTag.query.filter(and_(~SoundTag.sounds.any(), ~SoundTag.albums.any())).all(): db.session.delete(otag) # Fetch album, and associate if owner if album and (album != "__None"): db_album = Album.query.filter(Album.id == album).first() if db_album and (db_album.user_id == current_user.id): sound.album_id = db_album.id if not db_album.sounds: sound.album_order = 0 else: sound.album_order = db_album.sounds.count() + 1 elif album == "__None": sound.album_id = None sound.album_order = 0 db.session.commit() # trigger a sound update send_update_sound(sound) relationship = False if current_token and current_token.user: relationship = to_json_relationship(current_token.user, sound.user) account = to_json_account(sound.user, relationship) return jsonify(to_json_track(sound, account))
def show(username_or_id, soundslug): """ Get track details. --- tags: - Tracks parameters: - name: user_id in: path type: integer required: true description: User ID - name: soundslug in: path type: string required: true description: Track slug responses: 200: description: Returns track details. """ # Get logged in user from bearer token, or None if not logged in if current_token: current_user = current_token.user else: current_user = None # Get the associated User from url fetch track_user = User.query.filter(User.name == username_or_id, User.local.is_(True)).first() if not track_user: try: track_user = User.query.filter(User.flake_id == username_or_id).first() except sqlalchemy.exc.DataError: return jsonify({"error": "User not found"}), 404 if not track_user: return jsonify({"error": "User not found"}), 404 if current_user and (track_user.id == current_user.id): print("user") sound = Sound.query.filter(Sound.slug == soundslug, Sound.user_id == track_user.id).first() else: print("no user") sound = Sound.query.filter( Sound.slug == soundslug, Sound.user_id == track_user.id, Sound.transcode_state == Sound.TRANSCODE_DONE ).first() if not sound: print("mmmh") return jsonify({"error": "not found"}), 404 if sound.private: if current_user: if sound.user_id != current_user.id: return jsonify({"error": "forbidden"}), 403 else: return jsonify({"error": "forbidden"}), 403 relationship = False if current_token and current_token.user: relationship = to_json_relationship(current_token.user, sound.user) account = to_json_account(sound.user, relationship) return jsonify(to_json_track(sound, account))
def search(): """ Search. --- tags: - Global parameters: - name: q in: query type: string required: true description: search string responses: 200: description: fixme. """ # Get logged in user from bearer token, or None if not logged in if current_token: current_user = current_token.user else: current_user = None s = request.args.get("q", None) if not s: return jsonify({"error": "No search string provided"}), 400 # This is the old search endpoint and needs to be improved # Especially tracks and accounts needs to be returned in the right format, with the data helpers # Users should be searched from known Actors or fetched # URI should be searched from known activities or fetched # FTS, well, FTS needs to be implemented results = {"accounts": [], "sounds": [], "mode": None, "from": None} if current_user: results["from"] = current_user.name # Search for sounds # TODO: Implement FTS to get sounds search sounds = [] # Search for accounts accounts = [] is_user_at_account = RE_ACCOUNT.match(s) if s.startswith("https://"): # Try to match the URI from Activities in database results["mode"] = "uri" users = Actor.query.filter(Actor.meta_deleted.is_(False), Actor.url == s).all() elif is_user_at_account: # It matches [email protected], try to match it locally results["mode"] = "acct" user = is_user_at_account.group("user") instance = is_user_at_account.group("instance") users = Actor.query.filter(Actor.meta_deleted.is_(False), Actor.preferred_username == user, Actor.domain == instance).all() else: # It's a FTS search results["mode"] = "username" # Match actor username in database if current_user: users = (db.session.query(Actor, Follower).outerjoin( Follower, and_(Actor.id == Follower.target_id, Follower.actor_id == current_user.actor[0].id)).filter( or_(Actor.preferred_username.contains(s), Actor.name.contains(s))).filter( not_(Actor.id == current_user.actor[0].id)).all()) else: users = (db.session.query(Actor).filter( or_(Actor.preferred_username.contains(s), Actor.name.contains(s))).all()) # Handle the found users if len(users) > 0: for actor in users: relationship = False if current_user: relationship = to_json_relationship(current_user, actor.user) accounts.append(to_json_account(actor.user, relationship)) if len(accounts) <= 0: # Do a webfinger # TODO FIXME: We should do this only if https:// or user@account submitted # And rework it slightly differently since we needs to backend.fetch_iri() for https:// who # can match a Sound and not only an Actor current_app.logger.debug(f"webfinger for {s}") try: remote_actor_url = get_actor_url(s, debug=current_app.debug) # We need to get the remote Actor backend = ap.get_backend() iri = backend.fetch_iri(remote_actor_url) if iri: # We have fetched an unknown Actor # Save it in database and return it properly current_app.logger.debug( f"got remote actor URL {remote_actor_url}") act = ap.parse_activity(iri) fetched_actor, fetched_user = create_remote_actor(act) db.session.add(fetched_user) db.session.add(fetched_actor) db.session.commit() relationship = False if current_user: relationship = to_json_relationship( current_user, fetched_user) accounts.append(to_json_account(fetched_user, relationship)) results["mode"] = "webfinger" except (InvalidURLError, ValueError): current_app.logger.exception(f"Invalid AP URL: {s}") # Then test fetching as a "normal" Activity ? # Finally fill the results dict results["accounts"] = accounts # FIXME: handle exceptions if results["mode"] == "uri" and len(sounds) <= 0: backend = ap.get_backend() iri = backend.fetch_iri(s) if iri: # FIXME: Is INBOX the right choice here ? backend.save(Box.INBOX, iri) # Fetch again, but get it from database activity = Activity.query.filter(Activity.url == iri).first() if not activity: current_app.logger.exception("WTF Activity is not saved") else: from tasks import create_sound_for_remote_track, upload_workflow sound_id = create_sound_for_remote_track(activity) sound = Sound.query.filter(Sound.id == sound_id).one() upload_workflow.delay(sound.id) relationship = False if current_user: relationship = to_json_relationship(current_user, sound.user) acct = to_json_account(sound.user, relationship) sounds.append(to_json_track(sound, acct)) return jsonify({"who": s, "results": results})
def user_statuses(user_id): """ User statuses. --- tags: - Timelines parameters: - name: count in: query type: integer required: true description: count per page - name: with_muted in: query type: boolean required: true description: with muted users - name: page in: query type: integer description: page number responses: 200: description: Returns array of Status """ # Caveats: only handle public Sounds since we either federate (public) or no count = int(request.args.get("count", 20)) page = int(request.args.get("page", 1)) # Get associated user user = User.query.filter(User.id == user_id).first() if not user: abort(404) q = db.session.query(Activity, Sound).filter( Activity.type == "Create", Activity.payload[("object", "type")].astext == "Audio") q = q.filter(Activity.meta_deleted.is_(False)) q = q.filter(Activity.payload["to"].astext.contains( "https://www.w3.org/ns/activitystreams#Public")) q = q.filter(Activity.actor == user.actor[0].id) q = q.join(Sound, Sound.activity_id == Activity.id) q = q.order_by(Activity.creation_date.desc()) q = q.paginate(page=page, per_page=count) tracks = [] for t in q.items: if t.Sound: relationship = False if current_token and current_token.user: relationship = to_json_relationship(current_token.user, t.Sound.user) account = to_json_account(t.Sound.user, relationship) tracks.append(to_json_track(t.Sound, account)) else: print(t.Activity) resp = { "page": page, "page_size": count, "totalItems": q.total, "items": tracks, "totalPages": q.pages } return jsonify(resp)
def public(): """ Public or TWKN statuses. --- tags: - Timelines parameters: - name: count in: query type: integer required: true description: count - name: with_muted in: query type: boolean required: true description: with muted users - name: local in: query type: boolean description: local only or TWKN responses: 200: description: Returns array of Status """ # Caveats: only handle public Sounds since we either federate (public) or no paginated = request.args.get("paginated", False) count = int(request.args.get("count", 20)) local_only = request.args.get("local", False) q = db.session.query(Activity, Sound).filter( Activity.type == "Create", Activity.payload[("object", "type")].astext == "Audio" ) q = q.filter(Activity.meta_deleted.is_(False)) if local_only: q = q.filter(Activity.local.is_(True)) q = q.filter(Activity.payload["to"].astext.contains("https://www.w3.org/ns/activitystreams#Public")) q = q.join(Sound, Sound.activity_id == Activity.id) q = q.order_by(Activity.creation_date.desc()) if paginated: # Render timeline as paginated page = int(request.args.get("page", 1)) q = q.paginate(page=page, per_page=count) tracks = [] for t in q.items: if t.Sound: # TODO(dashie) FIXME can probably be moved out to the q.filter() if not t.Sound.transcode_state == Sound.TRANSCODE_DONE: continue relationship = False if current_token and current_token.user: relationship = to_json_relationship(current_token.user, t.Sound.user) account = to_json_account(t.Sound.user, relationship) tracks.append(to_json_track(t.Sound, account)) else: print(t.Activity) resp = {"page": page, "page_size": count, "totalItems": q.total, "items": tracks, "totalPages": q.pages} return jsonify(resp) else: # mastoapi compatible since_id = request.args.get("since_id") # since then we want the timeline if since_id: q = q.filter(Sound.flake_id > since_id) # then limit count q = q.limit(count) tracks = [] for t in q.all(): if t.Sound: relationship = False if current_token and current_token.user: relationship = to_json_relationship(current_token.user, t.Sound.user) account = to_json_account(t.Sound.user, relationship) tracks.append(to_json_track(t.Sound, account)) else: print(t.Activity) return jsonify(tracks)