invidious-mod/src/invidious/routes/api/v1/authenticated.cr

495 行
14 KiB
Crystal

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
user.preferences = Preferences.from_json(env.request.body || "{}")
rescue
end
Invidious::Database::Users.update_preferences(user)
env.response.status_code = 204
end
def self.export_invidious(env)
env.response.content_type = "application/json"
user = env.get("user").as(User)
return Invidious::User::Export.to_invidious(user)
end
def self.import_invidious(env)
user = env.get("user").as(User)
begin
if body = env.request.body
body = env.request.body.not_nil!.gets_to_end
else
body = "{}"
end
Invidious::User::Import.from_invidious(user, body)
rescue
end
env.response.status_code = 204
end
def self.get_history(env)
env.response.content_type = "application/json"
user = env.get("user").as(User)
page = env.params.query["page"]?.try &.to_i?.try &.clamp(0, Int32::MAX)
page ||= 1
max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE)
max_results ||= user.preferences.max_results
max_results ||= CONFIG.default_user_preferences.max_results
start_index = (page - 1) * max_results
if user.watched[start_index]?
watched = user.watched.reverse[start_index, max_results]
end
watched ||= [] of String
return watched.to_json
end
def self.mark_watched(env)
user = env.get("user").as(User)
if !user.preferences.watch_history
return error_json(409, "Watch history is disabled in preferences.")
end
id = env.params.url["id"]
if !id.match(/^[a-zA-Z0-9_-]{11}$/)
return error_json(400, "Invalid video id.")
end
Invidious::Database::Users.mark_watched(user, id)
env.response.status_code = 204
end
def self.mark_unwatched(env)
user = env.get("user").as(User)
if !user.preferences.watch_history
return error_json(409, "Watch history is disabled in preferences.")
end
id = env.params.url["id"]
if !id.match(/^[a-zA-Z0-9_-]{11}$/)
return error_json(400, "Invalid video id.")
end
Invidious::Database::Users.mark_unwatched(user, id)
env.response.status_code = 204
end
def self.clear_history(env)
user = env.get("user").as(User)
Invidious::Database::Users.clear_watch_history(user)
env.response.status_code = 204
end
def self.feed(env)
env.response.content_type = "application/json"
user = env.get("user").as(User)
locale = 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(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)
subscriptions = Invidious::Database::Channels.select(user.subscriptions)
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)
Invidious::Database::Users.subscribe_channel(user, ucid)
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"]
Invidious::Database::Users.unsubscribe_channel(user, ucid)
env.response.status_code = 204
end
def self.list_playlists(env)
env.response.content_type = "application/json"
user = env.get("user").as(User)
playlists = Invidious::Database::Playlists.select_all(author: user.email)
JSON.build do |json|
json.array do
playlists.each do |playlist|
playlist.to_json(0, json)
end
end
end
end
def self.create_playlist(env)
env.response.content_type = "application/json"
user = env.get("user").as(User)
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 { |p| PlaylistPrivacy.parse(p.as(String).downcase) }
if !privacy
return error_json(400, "Invalid privacy setting.")
end
if Invidious::Database::Playlists.count_owned_by(user.email) >= 100
return error_json(400, "User cannot have more than 100 playlists.")
end
playlist = create_playlist(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)
env.response.content_type = "application/json"
user = env.get("user").as(User)
plid = env.params.url["plid"]?
if !plid || plid.empty?
return error_json(400, "A playlist ID is required")
end
playlist = Invidious::Database::Playlists.select(id: plid)
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 { |p| PlaylistPrivacy.parse(p.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
Invidious::Database::Playlists.update(plid, title, privacy, description, updated)
env.response.status_code = 204
end
def self.delete_playlist(env)
env.response.content_type = "application/json"
user = env.get("user").as(User)
plid = env.params.url["plid"]
playlist = Invidious::Database::Playlists.select(id: plid)
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
Invidious::Database::Playlists.delete(plid)
env.response.status_code = 204
end
def self.insert_video_into_playlist(env)
env.response.content_type = "application/json"
user = env.get("user").as(User)
plid = env.params.url["plid"]
playlist = Invidious::Database::Playlists.select(id: plid)
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 >= CONFIG.playlist_length_limit
return error_json(400, "Playlist cannot have more than #{CONFIG.playlist_length_limit} 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)
rescue ex : NotFoundException
return error_json(404, ex)
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),
})
Invidious::Database::PlaylistVideos.insert(playlist_video)
Invidious::Database::Playlists.update_video_added(plid, playlist_video.index)
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
JSON.build do |json|
playlist_video.to_json(json, index: playlist.index.size)
end
end
def self.delete_video_in_playlist(env)
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 = Invidious::Database::Playlists.select(id: plid)
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
Invidious::Database::PlaylistVideos.delete(index)
Invidious::Database::Playlists.update_video_removed(plid, index)
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 = Invidious::Database::SessionIDs.select_all(user.email)
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 = env.get("preferences").as(Preferences).locale
case env.request.headers["Content-Type"]?
when "application/x-www-form-urlencoded"
scopes = env.params.body.select { |k, _| k.match(/^scopes\[\d+\]$/) }.map { |_, 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(&.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, use_nonce: true)
return templated "user/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)
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)
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)
Invidious::Database::SessionIDs.delete(sid: session)
elsif scopes_include_scope(scopes, "GET:tokens")
Invidious::Database::SessionIDs.delete(sid: session)
else
return error_json(400, "Cannot revoke session #{session}")
end
env.response.status_code = 204
end
def self.notifications(env)
env.response.content_type = "text/event-stream"
raw_topics = env.params.body["topics"]? || env.params.query["topics"]?
topics = raw_topics.try &.split(",").uniq.first(1000)
topics ||= [] of String
create_notification_stream(env, topics, CONNECTION_CHANNEL)
end
end