Browse Source
Extract API routes from invidious.cr (3/3)
Extract API routes from invidious.cr (3/3)
- Auth (excluding notifications*) APIs - Mixes *Notifications currently require the "connection_channel" channel for talking with the notifications job. Unfortunately, we cannot access that within the route modules yet.pull/2271/head
No known key found for this signature in database
GPG Key ID: 6FA616E5A5294A82
4 changed files with 573 additions and 527 deletions
-
529src/invidious.cr
-
412src/invidious/routes/api/v1/authenticated.cr
-
123src/invidious/routes/api/v1/misc.cr
-
36src/invidious/routes/api/v1/routes.cr
@ -0,0 +1,412 @@ |
|||
module Invidious::Routes::APIv1::Authenticated |
|||
# def self.notifications(env) |
|||
# env.response.content_type = "text/event-stream" |
|||
|
|||
# topics = env.params.body["topics"]?.try &.split(",").uniq.first(1000) |
|||
# topics ||= [] of String |
|||
|
|||
# create_notification_stream(env, topics, connection_channel) |
|||
# end |
|||
|
|||
def self.get_preferences(env) |
|||
env.response.content_type = "application/json" |
|||
user = env.get("user").as(User) |
|||
user.preferences.to_json |
|||
end |
|||
|
|||
def self.set_preferences(env) |
|||
env.response.content_type = "application/json" |
|||
user = env.get("user").as(User) |
|||
|
|||
begin |
|||
preferences = Preferences.from_json(env.request.body || "{}") |
|||
rescue |
|||
preferences = user.preferences |
|||
end |
|||
|
|||
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences.to_json, user.email) |
|||
|
|||
env.response.status_code = 204 |
|||
end |
|||
|
|||
def self.feed(env) |
|||
env.response.content_type = "application/json" |
|||
|
|||
user = env.get("user").as(User) |
|||
locale = LOCALES[env.get("preferences").as(Preferences).locale]? |
|||
|
|||
max_results = env.params.query["max_results"]?.try &.to_i? |
|||
max_results ||= user.preferences.max_results |
|||
max_results ||= CONFIG.default_user_preferences.max_results |
|||
|
|||
page = env.params.query["page"]?.try &.to_i? |
|||
page ||= 1 |
|||
|
|||
videos, notifications = get_subscription_feed(PG_DB, user, max_results, page) |
|||
|
|||
JSON.build do |json| |
|||
json.object do |
|||
json.field "notifications" do |
|||
json.array do |
|||
notifications.each do |video| |
|||
video.to_json(locale, json) |
|||
end |
|||
end |
|||
end |
|||
|
|||
json.field "videos" do |
|||
json.array do |
|||
videos.each do |video| |
|||
video.to_json(locale, json) |
|||
end |
|||
end |
|||
end |
|||
end |
|||
end |
|||
end |
|||
|
|||
def self.get_subscriptions(env) |
|||
env.response.content_type = "application/json" |
|||
user = env.get("user").as(User) |
|||
|
|||
if user.subscriptions.empty? |
|||
values = "'{}'" |
|||
else |
|||
values = "VALUES #{user.subscriptions.map { |id| %(('#{id}')) }.join(",")}" |
|||
end |
|||
|
|||
subscriptions = PG_DB.query_all("SELECT * FROM channels WHERE id = ANY(#{values})", as: InvidiousChannel) |
|||
|
|||
JSON.build do |json| |
|||
json.array do |
|||
subscriptions.each do |subscription| |
|||
json.object do |
|||
json.field "author", subscription.author |
|||
json.field "authorId", subscription.id |
|||
end |
|||
end |
|||
end |
|||
end |
|||
end |
|||
|
|||
def self.subscribe_channel(env) |
|||
env.response.content_type = "application/json" |
|||
user = env.get("user").as(User) |
|||
|
|||
ucid = env.params.url["ucid"] |
|||
|
|||
if !user.subscriptions.includes? ucid |
|||
get_channel(ucid, PG_DB, false, false) |
|||
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_append(subscriptions,$1) WHERE email = $2", ucid, user.email) |
|||
end |
|||
|
|||
# For Google accounts, access tokens don't have enough information to |
|||
# make a request on the user's behalf, which is why we don't sync with |
|||
# YouTube. |
|||
|
|||
env.response.status_code = 204 |
|||
end |
|||
|
|||
def self.unsubscribe_channel(env) |
|||
env.response.content_type = "application/json" |
|||
user = env.get("user").as(User) |
|||
|
|||
ucid = env.params.url["ucid"] |
|||
|
|||
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_remove(subscriptions, $1) WHERE email = $2", ucid, user.email) |
|||
|
|||
env.response.status_code = 204 |
|||
end |
|||
|
|||
def self.list_playlists(env) |
|||
locale = LOCALES[env.get("preferences").as(Preferences).locale]? |
|||
|
|||
env.response.content_type = "application/json" |
|||
user = env.get("user").as(User) |
|||
|
|||
playlists = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1", user.email, as: InvidiousPlaylist) |
|||
|
|||
JSON.build do |json| |
|||
json.array do |
|||
playlists.each do |playlist| |
|||
playlist.to_json(0, locale, json) |
|||
end |
|||
end |
|||
end |
|||
end |
|||
|
|||
def self.create_playlist(env) |
|||
env.response.content_type = "application/json" |
|||
user = env.get("user").as(User) |
|||
locale = LOCALES[env.get("preferences").as(Preferences).locale]? |
|||
|
|||
title = env.params.json["title"]?.try &.as(String).delete("<>").byte_slice(0, 150) |
|||
if !title |
|||
return error_json(400, "Invalid title.") |
|||
end |
|||
|
|||
privacy = env.params.json["privacy"]?.try { |privacy| PlaylistPrivacy.parse(privacy.as(String).downcase) } |
|||
if !privacy |
|||
return error_json(400, "Invalid privacy setting.") |
|||
end |
|||
|
|||
if PG_DB.query_one("SELECT count(*) FROM playlists WHERE author = $1", user.email, as: Int64) >= 100 |
|||
return error_json(400, "User cannot have more than 100 playlists.") |
|||
end |
|||
|
|||
playlist = create_playlist(PG_DB, title, privacy, user) |
|||
env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{playlist.id}" |
|||
env.response.status_code = 201 |
|||
{ |
|||
"title" => title, |
|||
"playlistId" => playlist.id, |
|||
}.to_json |
|||
end |
|||
|
|||
def self.update_playlist_attribute(env) |
|||
locale = LOCALES[env.get("preferences").as(Preferences).locale]? |
|||
|
|||
env.response.content_type = "application/json" |
|||
user = env.get("user").as(User) |
|||
|
|||
plid = env.params.url["plid"] |
|||
|
|||
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) |
|||
if !playlist || playlist.author != user.email && playlist.privacy.private? |
|||
return error_json(404, "Playlist does not exist.") |
|||
end |
|||
|
|||
if playlist.author != user.email |
|||
return error_json(403, "Invalid user") |
|||
end |
|||
|
|||
title = env.params.json["title"].try &.as(String).delete("<>").byte_slice(0, 150) || playlist.title |
|||
privacy = env.params.json["privacy"]?.try { |privacy| PlaylistPrivacy.parse(privacy.as(String).downcase) } || playlist.privacy |
|||
description = env.params.json["description"]?.try &.as(String).delete("\r") || playlist.description |
|||
|
|||
if title != playlist.title || |
|||
privacy != playlist.privacy || |
|||
description != playlist.description |
|||
updated = Time.utc |
|||
else |
|||
updated = playlist.updated |
|||
end |
|||
|
|||
PG_DB.exec("UPDATE playlists SET title = $1, privacy = $2, description = $3, updated = $4 WHERE id = $5", title, privacy, description, updated, plid) |
|||
env.response.status_code = 204 |
|||
end |
|||
|
|||
def self.delete_playlist(env) |
|||
locale = LOCALES[env.get("preferences").as(Preferences).locale]? |
|||
|
|||
env.response.content_type = "application/json" |
|||
user = env.get("user").as(User) |
|||
|
|||
plid = env.params.url["plid"] |
|||
|
|||
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) |
|||
if !playlist || playlist.author != user.email && playlist.privacy.private? |
|||
return error_json(404, "Playlist does not exist.") |
|||
end |
|||
|
|||
if playlist.author != user.email |
|||
return error_json(403, "Invalid user") |
|||
end |
|||
|
|||
PG_DB.exec("DELETE FROM playlist_videos * WHERE plid = $1", plid) |
|||
PG_DB.exec("DELETE FROM playlists * WHERE id = $1", plid) |
|||
|
|||
env.response.status_code = 204 |
|||
end |
|||
|
|||
def self.insert_video_into_playlist(env) |
|||
locale = LOCALES[env.get("preferences").as(Preferences).locale]? |
|||
|
|||
env.response.content_type = "application/json" |
|||
user = env.get("user").as(User) |
|||
|
|||
plid = env.params.url["plid"] |
|||
|
|||
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) |
|||
if !playlist || playlist.author != user.email && playlist.privacy.private? |
|||
return error_json(404, "Playlist does not exist.") |
|||
end |
|||
|
|||
if playlist.author != user.email |
|||
return error_json(403, "Invalid user") |
|||
end |
|||
|
|||
if playlist.index.size >= 500 |
|||
return error_json(400, "Playlist cannot have more than 500 videos") |
|||
end |
|||
|
|||
video_id = env.params.json["videoId"].try &.as(String) |
|||
if !video_id |
|||
return error_json(403, "Invalid videoId") |
|||
end |
|||
|
|||
begin |
|||
video = get_video(video_id, PG_DB) |
|||
rescue ex |
|||
return error_json(500, ex) |
|||
end |
|||
|
|||
playlist_video = PlaylistVideo.new({ |
|||
title: video.title, |
|||
id: video.id, |
|||
author: video.author, |
|||
ucid: video.ucid, |
|||
length_seconds: video.length_seconds, |
|||
published: video.published, |
|||
plid: plid, |
|||
live_now: video.live_now, |
|||
index: Random::Secure.rand(0_i64..Int64::MAX), |
|||
}) |
|||
|
|||
video_array = playlist_video.to_a |
|||
args = arg_array(video_array) |
|||
|
|||
PG_DB.exec("INSERT INTO playlist_videos VALUES (#{args})", args: video_array) |
|||
PG_DB.exec("UPDATE playlists SET index = array_append(index, $1), video_count = cardinality(index) + 1, updated = $2 WHERE id = $3", playlist_video.index, Time.utc, plid) |
|||
|
|||
env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{plid}/videos/#{playlist_video.index.to_u64.to_s(16).upcase}" |
|||
env.response.status_code = 201 |
|||
playlist_video.to_json(locale, index: playlist.index.size) |
|||
end |
|||
|
|||
def self.delete_video_in_playlist(env) |
|||
locale = LOCALES[env.get("preferences").as(Preferences).locale]? |
|||
|
|||
env.response.content_type = "application/json" |
|||
user = env.get("user").as(User) |
|||
|
|||
plid = env.params.url["plid"] |
|||
index = env.params.url["index"].to_i64(16) |
|||
|
|||
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) |
|||
if !playlist || playlist.author != user.email && playlist.privacy.private? |
|||
return error_json(404, "Playlist does not exist.") |
|||
end |
|||
|
|||
if playlist.author != user.email |
|||
return error_json(403, "Invalid user") |
|||
end |
|||
|
|||
if !playlist.index.includes? index |
|||
return error_json(404, "Playlist does not contain index") |
|||
end |
|||
|
|||
PG_DB.exec("DELETE FROM playlist_videos * WHERE index = $1", index) |
|||
PG_DB.exec("UPDATE playlists SET index = array_remove(index, $1), video_count = cardinality(index) - 1, updated = $2 WHERE id = $3", index, Time.utc, plid) |
|||
|
|||
env.response.status_code = 204 |
|||
end |
|||
|
|||
# Invidious::Routing.patch "/api/v1/auth/playlists/:plid/videos/:index" |
|||
# def modify_playlist_at(env) |
|||
# TODO |
|||
# end |
|||
|
|||
def self.get_tokens(env) |
|||
env.response.content_type = "application/json" |
|||
user = env.get("user").as(User) |
|||
scopes = env.get("scopes").as(Array(String)) |
|||
|
|||
tokens = PG_DB.query_all("SELECT id, issued FROM session_ids WHERE email = $1", user.email, as: {session: String, issued: Time}) |
|||
|
|||
JSON.build do |json| |
|||
json.array do |
|||
tokens.each do |token| |
|||
json.object do |
|||
json.field "session", token[:session] |
|||
json.field "issued", token[:issued].to_unix |
|||
end |
|||
end |
|||
end |
|||
end |
|||
end |
|||
|
|||
def self.register_token(env) |
|||
user = env.get("user").as(User) |
|||
locale = LOCALES[env.get("preferences").as(Preferences).locale]? |
|||
|
|||
case env.request.headers["Content-Type"]? |
|||
when "application/x-www-form-urlencoded" |
|||
scopes = env.params.body.select { |k, v| k.match(/^scopes\[\d+\]$/) }.map { |k, v| v } |
|||
callback_url = env.params.body["callbackUrl"]? |
|||
expire = env.params.body["expire"]?.try &.to_i? |
|||
when "application/json" |
|||
scopes = env.params.json["scopes"].as(Array).map { |v| v.as_s } |
|||
callback_url = env.params.json["callbackUrl"]?.try &.as(String) |
|||
expire = env.params.json["expire"]?.try &.as(Int64) |
|||
else |
|||
return error_json(400, "Invalid or missing header 'Content-Type'") |
|||
end |
|||
|
|||
if callback_url && callback_url.empty? |
|||
callback_url = nil |
|||
end |
|||
|
|||
if callback_url |
|||
callback_url = URI.parse(callback_url) |
|||
end |
|||
|
|||
if sid = env.get?("sid").try &.as(String) |
|||
env.response.content_type = "text/html" |
|||
|
|||
csrf_token = generate_response(sid, {":authorize_token"}, HMAC_KEY, PG_DB, use_nonce: true) |
|||
return templated "authorize_token" |
|||
else |
|||
env.response.content_type = "application/json" |
|||
|
|||
superset_scopes = env.get("scopes").as(Array(String)) |
|||
|
|||
authorized_scopes = [] of String |
|||
scopes.each do |scope| |
|||
if scopes_include_scope(superset_scopes, scope) |
|||
authorized_scopes << scope |
|||
end |
|||
end |
|||
|
|||
access_token = generate_token(user.email, authorized_scopes, expire, HMAC_KEY, PG_DB) |
|||
|
|||
if callback_url |
|||
access_token = URI.encode_www_form(access_token) |
|||
|
|||
if query = callback_url.query |
|||
query = HTTP::Params.parse(query.not_nil!) |
|||
else |
|||
query = HTTP::Params.new |
|||
end |
|||
|
|||
query["token"] = access_token |
|||
callback_url.query = query.to_s |
|||
|
|||
env.redirect callback_url.to_s |
|||
else |
|||
access_token |
|||
end |
|||
end |
|||
end |
|||
|
|||
def self.unregister_token(env) |
|||
locale = LOCALES[env.get("preferences").as(Preferences).locale]? |
|||
env.response.content_type = "application/json" |
|||
user = env.get("user").as(User) |
|||
scopes = env.get("scopes").as(Array(String)) |
|||
|
|||
session = env.params.json["session"]?.try &.as(String) |
|||
session ||= env.get("session").as(String) |
|||
|
|||
# Allow tokens to revoke other tokens with correct scope |
|||
if session == env.get("session").as(String) |
|||
PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", session) |
|||
elsif scopes_include_scope(scopes, "GET:tokens") |
|||
PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", session) |
|||
else |
|||
return error_json(400, "Cannot revoke session #{session}") |
|||
end |
|||
|
|||
env.response.status_code = 204 |
|||
end |
|||
end |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue