invidious-mod/src/invidious/users.cr

254 行
6.4 KiB
Crystal
Raw 通常表示 履歴

2018-08-05 05:30:44 +09:00
class User
module PreferencesConverter
def self.from_rs(rs)
begin
Preferences.from_json(rs.read(String))
rescue ex
DEFAULT_USER_PREFERENCES
end
end
end
add_mapping({
2018-08-16 02:40:42 +09:00
id: Array(String),
2018-08-05 05:30:44 +09:00
updated: Time,
notifications: Array(String),
subscriptions: Array(String),
email: String,
preferences: {
type: Preferences,
default: DEFAULT_USER_PREFERENCES,
converter: PreferencesConverter,
},
password: String?,
token: String,
watched: Array(String),
})
end
DEFAULT_USER_PREFERENCES = Preferences.from_json({
2018-08-31 06:49:38 +09:00
"video_loop" => false,
"autoplay" => false,
"speed" => 1.0,
"quality" => "hd720",
"volume" => 100,
"comments" => ["youtube", ""],
"captions" => ["", "", ""],
"related_videos" => true,
"dark_mode" => false,
2018-09-22 01:06:35 +09:00
"thin_mode" => false,
2018-08-31 06:49:38 +09:00
"max_results" => 40,
"sort" => "published",
"latest_only" => false,
"unseen_only" => false,
2018-08-05 05:30:44 +09:00
}.to_json)
class Preferences
2018-08-26 08:33:15 +09:00
module StringToArray
def self.to_json(value : Array(String), json : JSON::Builder)
2018-08-26 11:33:53 +09:00
json.array do
value.each do |element|
json.string element
end
end
2018-08-26 08:33:15 +09:00
end
def self.from_json(value : JSON::PullParser) : Array(String)
begin
result = [] of String
value.read_array do
result << value.read_string
end
rescue ex
result = [value.read_string, ""]
end
result
end
end
2018-08-05 05:30:44 +09:00
JSON.mapping({
video_loop: Bool,
autoplay: Bool,
2018-11-12 02:45:05 +09:00
continue: {
type: Bool,
default: false,
},
listen: {
2018-10-30 23:41:23 +09:00
type: Bool,
default: false,
},
speed: Float32,
quality: String,
volume: Int32,
comments: {
2018-08-26 08:33:15 +09:00
type: Array(String),
default: ["youtube", ""],
converter: StringToArray,
2018-08-05 05:30:44 +09:00
},
2018-08-07 03:23:36 +09:00
captions: {
type: Array(String),
default: ["", "", ""],
},
2018-08-05 05:30:44 +09:00
redirect_feed: {
type: Bool,
default: false,
},
2018-08-31 06:49:38 +09:00
related_videos: {
type: Bool,
default: true,
},
2018-08-05 05:30:44 +09:00
dark_mode: Bool,
thin_mode: {
type: Bool,
default: false,
},
max_results: Int32,
sort: String,
latest_only: Bool,
unseen_only: Bool,
notifications_only: {
type: Bool,
default: false,
},
})
end
def get_user(sid, client, headers, db, refresh = true)
2018-08-16 02:40:42 +09:00
if db.query_one?("SELECT EXISTS (SELECT true FROM users WHERE $1 = ANY(id))", sid, as: Bool)
user = db.query_one("SELECT * FROM users WHERE $1 = ANY(id)", sid, as: User)
2018-08-05 05:30:44 +09:00
if refresh && Time.now - user.updated > 1.minute
user = fetch_user(sid, client, headers, db)
user_array = user.to_a
user_array[5] = user_array[5].to_json
args = arg_array(user_array)
db.exec("INSERT INTO users VALUES (#{args}) \
2018-08-16 02:40:42 +09:00
ON CONFLICT (email) DO UPDATE SET id = users.id || $1, updated = $2, subscriptions = $4", user_array)
begin
view_name = "subscriptions_#{sha256(user.email)[0..7]}"
PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
SELECT * FROM channel_videos WHERE \
ucid = ANY ((SELECT subscriptions FROM users WHERE email = '#{user.email}')::text[]) \
ORDER BY published DESC;")
rescue ex
end
2018-08-05 05:30:44 +09:00
end
else
user = fetch_user(sid, client, headers, db)
user_array = user.to_a
user_array[5] = user_array[5].to_json
args = arg_array(user.to_a)
db.exec("INSERT INTO users VALUES (#{args}) \
2018-08-16 02:40:42 +09:00
ON CONFLICT (email) DO UPDATE SET id = users.id || $1, updated = $2, subscriptions = $4", user_array)
begin
view_name = "subscriptions_#{sha256(user.email)[0..7]}"
PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
SELECT * FROM channel_videos WHERE \
ucid = ANY ((SELECT subscriptions FROM users WHERE email = '#{user.email}')::text[]) \
ORDER BY published DESC;")
rescue ex
end
2018-08-05 05:30:44 +09:00
end
return user
end
def fetch_user(sid, client, headers, db)
feed = client.get("/subscription_manager?disable_polymer=1", headers)
feed = XML.parse_html(feed.body)
channels = [] of String
feed.xpath_nodes(%q(//ul[@id="guide-channels"]/li/a)).each do |channel|
if !{"Popular on YouTube", "Music", "Sports", "Gaming"}.includes? channel["title"]
2018-08-05 05:30:44 +09:00
channel_id = channel["href"].lstrip("/channel/")
begin
channel = get_channel(channel_id, client, db, false, false)
channels << channel.id
rescue ex
next
end
end
end
email = feed.xpath_node(%q(//a[@class="yt-masthead-picker-header yt-masthead-picker-active-account"]))
if email
email = email.content.strip
else
email = ""
end
token = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
2018-08-16 02:40:42 +09:00
user = User.new([sid], Time.now, [] of String, channels, email, DEFAULT_USER_PREFERENCES, nil, token, [] of String)
2018-08-05 05:30:44 +09:00
return user
end
def create_user(sid, email, password)
password = Crypto::Bcrypt::Password.create(password, cost: 10)
token = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
2018-08-16 02:40:42 +09:00
user = User.new([sid], Time.now, [] of String, [] of String, email, DEFAULT_USER_PREFERENCES, password.to_s, token, [] of String)
2018-08-05 05:30:44 +09:00
return user
end
2018-11-12 00:44:16 +09:00
def create_response(user_id, operation, key, expire = 6.hours)
expire = Time.now + expire
nonce = Random::Secure.hex(4)
challenge = "#{expire.to_unix}-#{nonce}-#{user_id}-#{operation}"
token = OpenSSL::HMAC.digest(:sha256, key, challenge)
challenge = Base64.urlsafe_encode(challenge)
token = Base64.urlsafe_encode(token)
return challenge, token
end
def validate_response(challenge, token, user_id, operation, key)
if !challenge
raise "Hidden field \"challenge\" is a required field"
end
if !token
raise "Hidden field \"token\" is a required field"
end
challenge = Base64.decode_string(challenge)
if challenge.split("-").size == 4
expire, nonce, challenge_user_id, challenge_operation = challenge.split("-")
expire = expire.to_i?
expire ||= 0
else
raise "Invalid challenge"
end
challenge = OpenSSL::HMAC.digest(:sha256, HMAC_KEY, challenge)
challenge = Base64.urlsafe_encode(challenge)
if challenge != token
raise "Invalid token"
end
if challenge_operation != operation
raise "Invalid token"
end
if challenge_user_id != user_id
raise "Invalid token"
end
if expire < Time.now.to_unix
raise "Token is expired, please try again"
end
end