2018-11-09 07:38:03 +05:30
require " crypto/bcrypt/password "
2019-07-08 00:30:42 +05:30
# Materialized views may not be defined using bound parameters (`$1` as used elsewhere)
2019-07-09 20:04:19 +05:30
MATERIALIZED_VIEW_SQL = - > ( email : String ) { " SELECT cv.* FROM channel_videos cv WHERE EXISTS (SELECT subscriptions FROM users u WHERE cv.ucid = ANY (u.subscriptions) AND u.email = E' #{ email . gsub ( { '\'' = > " \\ ' " , '\\' = > " \\ \\ " } ) } ') ORDER BY published DESC " }
2019-07-08 00:30:42 +05:30
2019-03-30 03:00:02 +05:30
struct User
2020-07-26 20:28:50 +05:30
include DB :: Serializable
property updated : Time
property notifications : Array ( String )
property subscriptions : Array ( String )
property email : String
@[ DB :: Field ( converter : User :: PreferencesConverter ) ]
property preferences : Preferences
property password : String ?
property token : String
property watched : Array ( String )
property feed_needs_update : Bool ?
2018-08-05 02:00:44 +05:30
module PreferencesConverter
def self . from_rs ( rs )
begin
Preferences . from_json ( rs . read ( String ) )
rescue ex
2019-03-29 00:13:40 +05:30
Preferences . from_json ( " {} " )
2018-08-05 02:00:44 +05:30
end
end
end
end
2021-12-07 02:58:16 +05:30
def get_user ( sid , headers , refresh = true )
2021-12-03 04:27:13 +05:30
if email = Invidious :: Database :: SessionIDs . select_email ( sid )
2021-12-03 06:57:51 +05:30
user = Invidious :: Database :: Users . select! ( email : email )
2018-08-05 02:00:44 +05:30
2019-06-08 06:26:41 +05:30
if refresh && Time . utc - user . updated > 1 . minute
2021-12-07 02:58:16 +05:30
user , sid = fetch_user ( sid , headers )
2019-02-11 00:03:29 +05:30
2021-12-03 06:57:51 +05:30
Invidious :: Database :: Users . insert ( user , update_on_conflict : true )
2021-12-03 04:27:13 +05:30
Invidious :: Database :: SessionIDs . insert ( sid , user . email , handle_conflicts : true )
2018-10-11 02:40:58 +05:30
begin
2019-04-11 06:26:38 +05:30
view_name = " subscriptions_ #{ sha256 ( user . email ) } "
2021-12-07 02:58:16 +05:30
PG_DB . exec ( " CREATE MATERIALIZED VIEW #{ view_name } AS #{ MATERIALIZED_VIEW_SQL . call ( user . email ) } " )
2018-10-11 02:40:58 +05:30
rescue ex
end
2018-08-05 02:00:44 +05:30
end
else
2021-12-07 02:58:16 +05:30
user , sid = fetch_user ( sid , headers )
2019-02-11 00:03:29 +05:30
2021-12-03 06:57:51 +05:30
Invidious :: Database :: Users . insert ( user , update_on_conflict : true )
2021-12-03 04:27:13 +05:30
Invidious :: Database :: SessionIDs . insert ( sid , user . email , handle_conflicts : true )
2018-10-11 02:40:58 +05:30
begin
2019-04-11 06:26:38 +05:30
view_name = " subscriptions_ #{ sha256 ( user . email ) } "
2021-12-07 02:58:16 +05:30
PG_DB . exec ( " CREATE MATERIALIZED VIEW #{ view_name } AS #{ MATERIALIZED_VIEW_SQL . call ( user . email ) } " )
2018-10-11 02:40:58 +05:30
rescue ex
end
2018-08-05 02:00:44 +05:30
end
2019-02-11 00:03:29 +05:30
return user , sid
2018-08-05 02:00:44 +05:30
end
2021-12-07 02:58:16 +05:30
def fetch_user ( sid , headers )
2019-10-25 22:28:16 +05:30
feed = YT_POOL . client & . get ( " /subscription_manager?disable_polymer=1 " , headers )
2018-08-05 02:00:44 +05:30
feed = XML . parse_html ( feed . body )
channels = [ ] of String
2019-01-03 06:58:01 +05:30
channels = feed . xpath_nodes ( % q ( / / ul [ @id = " guide-channels " ] / li / a ) ) . compact_map do | channel |
if { " Popular on YouTube " , " Music " , " Sports " , " Gaming " } . includes? channel [ " title " ]
nil
else
channel [ " href " ] . lstrip ( " /channel/ " )
2018-08-05 02:00:44 +05:30
end
end
2021-12-07 08:10:15 +05:30
channels = get_batch_channels ( channels , false , false )
2019-01-03 06:58:01 +05:30
2018-08-05 02:00:44 +05:30
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 ) )
2020-07-26 20:28:50 +05:30
user = User . new ( {
updated : Time . utc ,
notifications : [ ] of String ,
subscriptions : channels ,
email : email ,
preferences : Preferences . new ( CONFIG . default_user_preferences . to_tuple ) ,
password : nil ,
token : token ,
watched : [ ] of String ,
feed_needs_update : true ,
} )
2019-02-11 00:03:29 +05:30
return user , sid
2018-08-05 02:00:44 +05:30
end
def create_user ( sid , email , password )
password = Crypto :: Bcrypt :: Password . create ( password , cost : 10 )
token = Base64 . urlsafe_encode ( Random :: Secure . random_bytes ( 32 ) )
2020-07-26 20:28:50 +05:30
user = User . new ( {
updated : Time . utc ,
notifications : [ ] of String ,
subscriptions : [ ] of String ,
email : email ,
preferences : Preferences . new ( CONFIG . default_user_preferences . to_tuple ) ,
password : password . to_s ,
token : token ,
watched : [ ] of String ,
feed_needs_update : true ,
} )
2018-08-05 02:00:44 +05:30
2019-02-11 00:03:29 +05:30
return user , sid
2018-08-05 02:00:44 +05:30
end
2018-11-11 21:14:16 +05:30
2021-12-07 02:58:16 +05:30
def generate_captcha ( key )
2018-11-26 05:56:21 +05:30
second = Random :: Secure . rand ( 12 )
second_angle = second * 30
second = second * 5
2018-11-18 00:48:12 +05:30
minute = Random :: Secure . rand ( 12 )
minute_angle = minute * 30
minute = minute * 5
hour = Random :: Secure . rand ( 12 )
hour_angle = hour * 30 + minute_angle . to_f / 12
if hour == 0
hour = 12
end
clock_svg = <<-END_SVG
2019-10-28 20:16:59 +05:30
< svg viewBox = " 0 0 100 100 " width = " 200px " height = " 200px " >
2018-11-18 00:48:12 +05:30
< circle cx = " 50 " cy = " 50 " r = " 45 " fill = " # eee " stroke = " black " stroke - width = " 2 " > < / circle>
2019-03-24 00:35:13 +05:30
2018-11-18 00:48:12 +05:30
< text x = " 69 " y = " 20.091 " text - anchor = " middle " fill = " black " font - family = " Arial " font - size = " 10px " > 1 < / text>
< text x = " 82.909 " y = " 34 " text - anchor = " middle " fill = " black " font - family = " Arial " font - size = " 10px " > 2 < / text>
< text x = " 88 " y = " 53 " text - anchor = " middle " fill = " black " font - family = " Arial " font - size = " 10px " > 3 < / text>
< text x = " 82.909 " y = " 72 " text - anchor = " middle " fill = " black " font - family = " Arial " font - size = " 10px " > 4 < / text>
< text x = " 69 " y = " 85.909 " text - anchor = " middle " fill = " black " font - family = " Arial " font - size = " 10px " > 5 < / text>
< text x = " 50 " y = " 91 " text - anchor = " middle " fill = " black " font - family = " Arial " font - size = " 10px " > 6 < / text>
< text x = " 31 " y = " 85.909 " text - anchor = " middle " fill = " black " font - family = " Arial " font - size = " 10px " > 7 < / text>
< text x = " 17.091 " y = " 72 " text - anchor = " middle " fill = " black " font - family = " Arial " font - size = " 10px " > 8 < / text>
< text x = " 12 " y = " 53 " text - anchor = " middle " fill = " black " font - family = " Arial " font - size = " 10px " > 9 < / text>
< text x = " 17.091 " y = " 34 " text - anchor = " middle " fill = " black " font - family = " Arial " font - size = " 10px " > 10 < / text>
< text x = " 31 " y = " 20.091 " text - anchor = " middle " fill = " black " font - family = " Arial " font - size = " 10px " > 11 < / text>
< text x = " 50 " y = " 15 " text - anchor = " middle " fill = " black " font - family = " Arial " font - size = " 10px " > 12 < / text>
< circle cx = " 50 " cy = " 50 " r = " 3 " fill = " black " > < / circle>
2018-11-26 05:56:21 +05:30
< line id = " second " transform = " rotate( #{ second_angle } , 50, 50) " x1 = " 50 " y1 = " 50 " x2 = " 50 " y2 = " 12 " fill = " black " stroke = " black " stroke - width = " 1 " > < / line>
2018-11-18 00:48:12 +05:30
< line id = " minute " transform = " rotate( #{ minute_angle } , 50, 50) " x1 = " 50 " y1 = " 50 " x2 = " 50 " y2 = " 16 " fill = " black " stroke = " black " stroke - width = " 2 " > < / line>
< line id = " hour " transform = " rotate( #{ hour_angle } , 50, 50) " x1 = " 50 " y1 = " 50 " x2 = " 50 " y2 = " 24 " fill = " black " stroke = " black " stroke - width = " 2 " > < / line>
< / svg>
END_SVG
image = " "
2019-10-28 20:16:59 +05:30
convert = Process . run ( %( rsvg-convert -w 400 -h 400 -b none -f png ) , shell : true ,
2018-11-18 00:48:12 +05:30
input : IO :: Memory . new ( clock_svg ) , output : Process :: Redirect :: Pipe ) do | proc |
image = proc . output . gets_to_end
image = Base64 . strict_encode ( image )
image = " data:image/png;base64, #{ image } "
end
2018-11-26 05:56:21 +05:30
answer = " #{ hour } : #{ minute . to_s . rjust ( 2 , '0' ) } : #{ second . to_s . rjust ( 2 , '0' ) } "
2018-11-18 00:48:12 +05:30
answer = OpenSSL :: HMAC . hexdigest ( :sha256 , key , answer )
2019-03-20 02:43:23 +05:30
return {
question : image ,
2021-12-07 02:58:16 +05:30
tokens : { generate_response ( answer , { " :login " } , key , use_nonce : true ) } ,
2019-03-20 02:43:23 +05:30
}
end
2021-12-07 02:58:16 +05:30
def generate_text_captcha ( key )
2021-01-08 01:39:24 +05:30
response = make_client ( TEXTCAPTCHA_URL , & . get ( " /github.com/iv.org/invidious.json " ) . body )
2019-03-20 02:43:23 +05:30
response = JSON . parse ( response )
tokens = response [ " a " ] . as_a . map do | answer |
2021-12-07 02:58:16 +05:30
generate_response ( answer . as_s , { " :login " } , key , use_nonce : true )
2019-03-20 02:43:23 +05:30
end
2018-11-18 00:48:12 +05:30
2019-03-20 02:43:23 +05:30
return {
question : response [ " q " ] . as_s ,
tokens : tokens ,
}
2018-11-18 00:48:12 +05:30
end
2019-05-15 22:56:29 +05:30
def subscribe_ajax ( channel_id , action , env_headers )
headers = HTTP :: Headers . new
headers [ " Cookie " ] = env_headers [ " Cookie " ]
2019-10-25 22:28:16 +05:30
html = YT_POOL . client & . get ( " /subscription_manager?disable_polymer=1 " , headers )
2019-05-15 22:56:29 +05:30
2021-05-24 19:15:50 +05:30
cookies = HTTP :: Cookies . from_client_headers ( headers )
2019-05-15 22:56:29 +05:30
html . cookies . each do | cookie |
if { " VISITOR_INFO1_LIVE " , " YSC " , " SIDCC " } . includes? cookie . name
if cookies [ cookie . name ]?
cookies [ cookie . name ] = cookie
else
cookies << cookie
end
end
end
headers = cookies . add_request_headers ( headers )
2020-06-16 04:03:23 +05:30
if match = html . body . match ( / 'XSRF_TOKEN': "(?<session_token>[^"]+)" / )
2019-05-15 22:56:29 +05:30
session_token = match [ " session_token " ]
headers [ " content-type " ] = " application/x-www-form-urlencoded "
post_req = {
2019-06-08 06:26:41 +05:30
session_token : session_token ,
2019-05-15 22:56:29 +05:30
}
post_url = " /subscription_ajax? #{ action } =1&c= #{ channel_id } "
2019-10-25 22:28:16 +05:30
YT_POOL . client & . post ( post_url , headers , form : post_req )
2019-05-15 22:56:29 +05:30
end
end
2019-06-07 23:09:12 +05:30
def get_subscription_feed ( db , user , max_results = 40 , page = 1 )
limit = max_results . clamp ( 0 , MAX_ITEMS_PER_PAGE )
offset = ( page - 1 ) * limit
2021-12-03 07:59:52 +05:30
notifications = Invidious :: Database :: Users . select_notifications ( user )
2019-06-07 23:09:12 +05:30
view_name = " subscriptions_ #{ sha256 ( user . email ) } "
if user . preferences . notifications_only && ! notifications . empty?
# Only show notifications
2021-12-02 23:46:41 +05:30
notifications = Invidious :: Database :: ChannelVideos . select ( notifications )
2019-06-07 23:09:12 +05:30
videos = [ ] of ChannelVideo
2021-09-25 08:12:43 +05:30
notifications . sort_by! ( & . published ) . reverse!
2019-06-07 23:09:12 +05:30
case user . preferences . sort
when " alphabetically "
2021-09-25 08:12:43 +05:30
notifications . sort_by! ( & . title )
2019-06-07 23:09:12 +05:30
when " alphabetically - reverse "
2021-09-25 08:12:43 +05:30
notifications . sort_by! ( & . title ) . reverse!
2019-06-07 23:09:12 +05:30
when " channel name "
2021-09-25 08:12:43 +05:30
notifications . sort_by! ( & . author )
2019-06-07 23:09:12 +05:30
when " channel name - reverse "
2021-09-25 08:12:43 +05:30
notifications . sort_by! ( & . author ) . reverse!
2020-04-09 22:48:09 +05:30
else nil # Ignore
2019-06-07 23:09:12 +05:30
end
else
if user . preferences . latest_only
if user . preferences . unseen_only
# Show latest video from a channel that a user hasn't watched
# "unseen_only" isn't really correct here, more accurate would be "unwatched_only"
if user . watched . empty?
values = " '{}' "
else
values = " VALUES #{ user . watched . map { | id | %( ( ' #{ id } ' ) ) } . join ( " , " ) } "
end
2019-08-27 18:38:26 +05:30
videos = PG_DB . query_all ( " SELECT DISTINCT ON (ucid) * FROM #{ view_name } WHERE NOT id = ANY ( #{ values } ) ORDER BY ucid, published DESC " , as : ChannelVideo )
2019-06-07 23:09:12 +05:30
else
# Show latest video from each channel
2019-08-27 18:38:26 +05:30
videos = PG_DB . query_all ( " SELECT DISTINCT ON (ucid) * FROM #{ view_name } ORDER BY ucid, published DESC " , as : ChannelVideo )
2019-06-07 23:09:12 +05:30
end
2021-09-25 08:12:43 +05:30
videos . sort_by! ( & . published ) . reverse!
2019-06-07 23:09:12 +05:30
else
if user . preferences . unseen_only
# Only show unwatched
if user . watched . empty?
values = " '{}' "
else
values = " VALUES #{ user . watched . map { | id | %( ( ' #{ id } ' ) ) } . join ( " , " ) } "
end
2019-08-27 18:38:26 +05:30
videos = PG_DB . query_all ( " SELECT * FROM #{ view_name } WHERE NOT id = ANY ( #{ values } ) ORDER BY published DESC LIMIT $1 OFFSET $2 " , limit , offset , as : ChannelVideo )
2019-06-07 23:09:12 +05:30
else
# Sort subscriptions as normal
2019-08-27 18:38:26 +05:30
videos = PG_DB . query_all ( " SELECT * FROM #{ view_name } ORDER BY published DESC LIMIT $1 OFFSET $2 " , limit , offset , as : ChannelVideo )
2019-06-07 23:09:12 +05:30
end
end
case user . preferences . sort
when " published - reverse "
2021-09-25 08:12:43 +05:30
videos . sort_by! ( & . published )
2019-06-07 23:09:12 +05:30
when " alphabetically "
2021-09-25 08:12:43 +05:30
videos . sort_by! ( & . title )
2019-06-07 23:09:12 +05:30
when " alphabetically - reverse "
2021-09-25 08:12:43 +05:30
videos . sort_by! ( & . title ) . reverse!
2019-06-07 23:09:12 +05:30
when " channel name "
2021-09-25 08:12:43 +05:30
videos . sort_by! ( & . author )
2019-06-07 23:09:12 +05:30
when " channel name - reverse "
2021-09-25 08:12:43 +05:30
videos . sort_by! ( & . author ) . reverse!
2020-04-09 22:48:09 +05:30
else nil # Ignore
2019-06-07 23:09:12 +05:30
end
2021-12-03 07:59:52 +05:30
notifications = Invidious :: Database :: Users . select_notifications ( user )
2019-06-07 23:09:12 +05:30
notifications = videos . select { | v | notifications . includes? v . id }
videos = videos - notifications
end
return videos , notifications
end