diff options
Diffstat (limited to 'src/invidious/routes/api/v1/authenticated.cr')
| -rw-r--r-- | src/invidious/routes/api/v1/authenticated.cr | 415 |
1 files changed, 415 insertions, 0 deletions
diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr new file mode 100644 index 00000000..b4e9e9c8 --- /dev/null +++ b/src/invidious/routes/api/v1/authenticated.cr @@ -0,0 +1,415 @@ +module Invidious::Routes::API::V1::Authenticated + # The notification APIs cannot be extracted yet! + # They require the *local* notifications constant defined in invidious.cr + # + # 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 |
