diff options
-rw-r--r-- | src/twitcr/client.cr | 15 | ||||
-rw-r--r-- | src/twitcr/mappings/game.cr | 13 | ||||
-rw-r--r-- | src/twitcr/rest.cr | 64 | ||||
-rw-r--r-- | util/twitch.cr | 185 |
4 files changed, 163 insertions, 114 deletions
diff --git a/src/twitcr/client.cr b/src/twitcr/client.cr index 1a02a40..a0d4be2 100644 --- a/src/twitcr/client.cr +++ b/src/twitcr/client.cr @@ -3,12 +3,13 @@ require "./mappings/*" class Twitcr::Client - getter token : String? - getter client_id : String - getter client_secret : String + #getter token : String? + #getter client_id : String + #getter client_secret : String - def initialize(@client_secret, @client_id) - @token = nil + #def initialize(@client_secret, @client_id) + # @token = nil + #end include REST @@ -48,11 +49,11 @@ class Twitcr::Client end def game_id( name : String ) - name.to_u64{ game(name).id } + game(name).id end def game_id( id : UInt64 ) - + self end def unfollow( unfollower : ( String | UInt64 ), unfollowee : ( String | UInt64 ) ) diff --git a/src/twitcr/mappings/game.cr b/src/twitcr/mappings/game.cr index 2915eb5..d0818cd 100644 --- a/src/twitcr/mappings/game.cr +++ b/src/twitcr/mappings/game.cr @@ -2,15 +2,16 @@ require "./converters" module Twitcr struct GameList - JSON.mapping({data: Array(Game)}) + include JSON::Serializable + property data : Array(Game) end struct Game - JSON.mapping({ - id: {type: UInt64, converter: ID::Converter}, - name: String, - box_art_url: String, - }) + include JSON::Serializable + @[JSON::Field(converter: ID::Converter)] + property id : UInt64 + property name : String + property box_art_url : String end end diff --git a/src/twitcr/rest.cr b/src/twitcr/rest.cr index 0fc59c1..ae958a7 100644 --- a/src/twitcr/rest.cr +++ b/src/twitcr/rest.cr @@ -143,27 +143,43 @@ module Twitcr::REST if ! query_string.empty?; query_string = "?" + query_string end request( "GET", Api::Kraken, "/streams" + query_string ) end - def get_streams_followed( ); request( "GET", Api::Kraken, "/streams/followed?limit=100&stream_type=all" ) end + #deprecated + #def get_streams_followed( ); request( "GET", Api::Kraken, "/streams/followed?limit=100&stream_type=all" ) end + def get_streams_followed( user_id : UInt64 ); request( "GET", Api::Helix, "/streams/followed?user_id=#{user_id}" ) end # FIXME: Deal with pagination def get_banned( broadcaster_id : UInt64 ); request( "GET", Api::Helix, "/moderation/banned?broadcaster_id=#{broadcaster_id.to_s}" ) end def get_banned( broadcaster_id : UInt64, user_id : UInt64 ); request( "GET", Api::Helix, "/moderation/banned?broadcaster_id=#{broadcaster_id.to_s}&user_id=#{user_id}" ) end def get_banned( broadcaster_id : UInt64, user_id : Array( UInt64 ) ); request( "GET", Api::Helix, "/moderation/banned?broadcaster_id=#{broadcaster_id.to_s}&user_id=#{user_id.join("&user_id=")}" ) end def get_banned_events( broadcaster_id : UInt64, first : UInt64 = 100 ); request( "GET", Api::Helix, "/moderation/banned/events?broadcaster_id=#{broadcaster_id.to_s}&first=#{first.to_s}" ) end def get_clips( name : String ); request( "GET", Api::Kraken, "/clips/top?channel=#{name}&first=100&period=all" ) end +# def get_videos( *, id : UInt64, user_id : UInt64, game_id : UInt64 ) +# params = Hash( String, String ).new +# params["first"] = "100" +# params["id"] = id.to_s +# params["user_id"] = user_id.to_s +# params["game_id"] = game_id.to_s +# if after ; params["after"] = after end +# query_string = HTTP::Params.encode( params ) +# if ! query_string.empty?; query_string = "?" + query_string end +# request( "GET", Api::Helix, "/videos" + query_string ) +# end def get_videos( id : UInt64, sort : String = "time", type : String = "all" ); request( "GET", Api::Helix, "/videos?user_id=#{id.to_s}&first=100&sort=#{sort}&type=#{type}" ) end def get_channels_videos( id : UInt64, sort : String = "time", type : String = "all" ); request( "GET", Api::Kraken, "/channels/#{id.to_s}/videos?limit=100&sort=#{sort}&broadcast_type=#{type}" ) end - def get_channel( ); request( "GET", Api::Kraken, "/channel" ) end - def get_channel( id : UInt64 ); request( "GET", Api::Kraken, "/channels/#{id.to_s}" ) end - def put_channel!( id : UInt64, *, delay : String? = nil, game : String? = nil, status : String? = nil); - body = Hash( String, Hash( String, String ) ).new - body["channel"] = Hash( String, String).new - if game ; body["channel"]["game"] = game end - if status ; body["channel"]["status"] = status end - if delay ; body["channel"]["delay"] = delay end - request( "PUT", Api::Kraken, "/channels/#{id.to_s}", body.to_json, contenttype: "application/json") end + #def get_channel( ); request( "GET", Api::Kraken, "/channel" ) end + def get_channel( id : UInt64 ); request( "GET", Api::Helix, "/channels/?broadcaster_id=#{id.to_s}" ) end + def put_channel!( id : UInt64, *, delay : String? = nil, game : String? = nil, title : String? = nil, lang : String? = nil ); + #body = Hash( String, Hash( String, String ) ).new + body = Hash( String, String ).new + #body["channel"] = Hash( String, String).new + game && ( body["game_id"] = game_id( game ).to_s ) + lang && ( body["lang"] = lang ) + title && ( body["title"] = title ) + delay && ( body["delay"] = delay ) + pp body.to_json + request( "PATCH", Api::Helix, "/channels/?broadcaster_id=#{id.to_s}", body.to_json, contenttype: "application/json") end def delete_key!( id : UInt64); request( "DELETE", Api::Kraken, "/channels/#{id.to_s}/stream_key" ) end def get_hosts( id : UInt64 ); request( "GET", Api::Kraken, "/channels/#{id.to_s}/hosts" ) end - def get_subs( id : UInt64 ); request( "GET", Api::Kraken, "/channels/#{id.to_s}/subscriptions" ) end + def get_subs( id : UInt64 ); request( "GET", Api::Helix, "/subscriptions?first=100&broadcaster_id=#{id}" ) end def get_eventsubs( ); request( "GET", Api::Helix, "/eventsub/subscriptions" ) end def delete_eventsubs!( id : String ); request( "DELETE", Api::Helix, "/eventsub/subscriptions?id=#{id}" ) end @@ -172,14 +188,11 @@ module Twitcr::REST varname = "broadcaster_user_id" case type when "channel.raid" - version = "beta" varname = "to_broadcaster_user_id" when "user.update" varname = "user_id" end - body = Hash( String, String | Hash( String, String ) ).new - - body = { + body = Hash( String, Bool | String | Hash( String, String ) ){ "type" => type, "version" => version, "condition" => { varname => id.to_s }, @@ -189,8 +202,11 @@ module Twitcr::REST "secret" => @settings["eventsub_secret"] # FIXME }, } + if type == "drop.entitlement.grant" + body["is_batching_enabled"] = true + end request( "POST", Api::Helix, "/eventsub/subscriptions", body.to_json, contenttype: "application/json" ) end - def get_app_token( ); request( "POST", Api::Oauth2, "/token?client_id=#{@settings["client_id"].not_nil!}&client_secret=#{@settings["client_secret"].not_nil!}&grant_type=client_credentials" ) end + def get_app_token( ); request( "POST", Api::Oauth2, "/token?client_id=#{@settings["client_id"].not_nil!}&client_secret=#{@settings["client_secret"].not_nil!}&grant_type=client_credentials&scope=#{ @settings["scopes"].split("\n").join("%20") }" ) end def get_token_validation( ); request( "GET", Api::Oauth2, "/validate" ) end # These APIs might require the official Twitch.com web Client-ID now? def get_panels( login : String ); request( "GET", Api::Api, "/channels/#{login}/panels" ) end @@ -199,7 +215,7 @@ module Twitcr::REST def unfriend!( friender : UInt64, friendee : UInt64 ); request( "DELETE", Api::Kraken, "/users/#{friender.to_s}/friends/#{friendee.to_s}" ) end def get_friends( friender : UInt64 ); request( "GET", Api::Kraken, "/users/#{friender.to_s}/friends" ) end - def parse_game( game : UInt64 | String ) + def parse_game( game : UInt64 | String ) list = GameList.from_json( get_games( game ) ) raise EMPTY_RESULT if list.data.empty? list.data.first @@ -211,15 +227,15 @@ module Twitcr::REST list.data.first end - def get_token + def authorize( ) response = HTTP::Client.exec( - "POST", - "https://id.twitch.tv/oauth2/token?client_id=#{@settings["client_id"].not_nil!}&client_secret=#{@settings["client_secret"].not_nil!}&grant_type=client_credentials", + "GET", + "https://id.twitch.tv/oauth2/authorize?client_id=#{@settings["client_id"].not_nil!}&scope=#{ @settings["scopes"].split("\n").join("%20") }&redirect_uri=#{@settings["redirect_uri"]}&response_type=code", tls: SSL_CONTEXT ) - - access_token = Token.from_json(response.body).access_token - - @token = "Bearer #{access_token}" + response.headers + end + def get_token( code : String ) + request("POST", Api::Oauth2, "/token?client_id=#{@settings["client_id"].not_nil!}&client_secret=#{@settings["client_secret"].not_nil!}&code=#{code}&grant_type=authorization_code&redirect_uri=#{@settings["redirect_uri"]}" ) end end diff --git a/util/twitch.cr b/util/twitch.cr index 1e63736..dea1ef1 100644 --- a/util/twitch.cr +++ b/util/twitch.cr @@ -7,7 +7,7 @@ require "pretty_print" settings = Hash(String, String).new settings["home"] = Path.home.to_s -["access_token", "channel", "channel_id", "client_id", "client_id_twitch", "client_secret", "app_access_token", "eventsub_secret"].each do |key| +["access_token", "channel", "channel_id", "client_id", "client_id_twitch", "client_secret", "app_access_token", "eventsub_secret", "scopes", "redirect_uri"].each do |key| begin settings[key] = File.read( settings["home"] + "/.config/twitch/" + key ).chomp rescue @@ -20,9 +20,7 @@ command = ARGV[0] client = Twitcr::Client.new( settings ) case command -when "videos", "lsvideo", "lsvideos" - list_videos_kraken( settings, client ) -when "videos_helix" # smaller json, less information +when "videos", "lsvideo", "lsvideos", "videos_helix" # smaller json, less information list_videos_helix( settings, client ) when "follows", "lsfollow", "lsfollows" list_user_follows( settings, client ) @@ -37,21 +35,23 @@ when "follow" when "unfollow" pp client.unfollow( settings["channel_id"], ARGV[1] ) when "streams_followed" - list_streams_followed( settings, client ) + list_streams_followed( settings, client, ARGV[1]? || settings["channel_id"].to_u64 ) when "clips" pp JSON.parse( client.get_clips( ARGV[1] ) ) when "game" - pp client.get_games( ARGV[1] ) + pp JSON.parse( client.get_games( ARGV[1].to_u64? || ARGV[1] ) ) when "streams" - list_streams_v5( settings, client, ARGV[1] ) -when "streams_helix" list_streams( settings, JSON.parse( client.get_streams( game_id = client.game_id( ARGV[1] ) ) ) ) +when "streams_kraken" + list_streams_v5( settings, client, ARGV[1] ) when "hosts" list_hosts( settings, client ) when "user" pp JSON.parse( client.get_user( ARGV[1] ) ) when "subs" - pp JSON.parse( client.get_subs( client.user( ARGV[1] ).id ) ) + list_subs( settings, client, ARGV[1] ) + + #pp JSON.parse( client.get_subs( client.user( ARGV[1] ).id ) ) when "eventsubs" pp JSON.parse( client.get_eventsubs( ) ) when "eventsub" @@ -60,20 +60,20 @@ when "eventunsub" pp client.delete_eventsubs!( ARGV[1] ) when "panels" pp JSON.parse( client.get_panels( ARGV[1] ) ) +when "authorize" + pp client.authorize() when "validate" pp JSON.parse( client.get_token_validation() ) when "app_token" pp JSON.parse( client.get_app_token() ) +when "token" + pp client.get_token( ARGV[1] ) when "reset_key" pp JSON.parse( client.delete_key!( settings["channel_id"].to_u64 ) ) when "update" update_channel( settings, client, settings["channel_id"].to_u64 ) when "channel" - if ARGV.size > 1 - pp JSON.parse( client.get_channel( client.user_id( ARGV[1] ) ) ) - else - pp JSON.parse( client.get_channel( ) ) - end + pp JSON.parse( client.get_channel( client.user_id( ARGV[1]? || settings["channel_id"] ) ) ) else puts "ARGV[0] must be one of videos, follows" end @@ -89,14 +89,18 @@ macro fcell( data ) if celi == 0 # preserve integrity of primary index at expense of last cell celw[-1] = celw[-1] - ( celw.sum( foh ) - winsz ) - elsif celi != celw.size - celw[celw.size] + elsif celi != ( celw.size - 1 ) + celw[( celw.size - 1 ) ] else celw[celi] = celw[celi] - ( celw.sum( foh ) - winsz ) - ( {{data}}.bytesize - {{data}}.size ) + if celw[celi] < 0 + celw[celi] = 0 + end end end celi+=1 { celw[celi-1], celw[celi-1], {{data}} } + {{debug()}} end macro get_winsz() @@ -111,6 +115,7 @@ def list_videos_helix( settings, client ) id = client.user( ARGV[1] ).id response = JSON.parse( client.get_videos( id ) ) + pp response winsz = get_winsz() @@ -129,61 +134,40 @@ def list_videos_helix( settings, client ) celi=0 printf( format, - *fcell( video["url"].as_s[12..] ), - *fcell( video["created_at"].as_s.[0..15] ), + *fcell( video["url"].as_s ), + *fcell( video["created_at"].as_s[0..15] ), *fcell( video["type"].as_s[0].to_s ), *fcell( video["duration"].as_s[ /([0-9]+[a-z]+)/, 1 ] ), *fcell( video["view_count"].as_i.to_s ), - # helix doesn't report game?! - # *fcell( video["game"].as_s ), - *fcell( video["title"].as_s.chomp ), + *fcell( video["title"].as_s.chomp), *fcell( video["description"].as_s.chomp ), - ) - - end - -end - -def list_videos_kraken( settings, client ) - - # FIXME: handle UInt32 more gracefully - # also: implement Twitcr::User so we can restrict valid strings - id = client.user( ARGV[1] ).id - - response = JSON.parse( client.get_channels_videos( id ) ) - - winsz = get_winsz() - - # cell widths - celw=[0,0,0,0,0,0,0,0] - # cell index - celi=0 - - format = "%*.*s %-*.*s %*.*s %*.*s %*.*s %-*.*s %-*.*s %-*.*s\n" - - # format overhead - foh = format.delete( "^ " ).size - - response["videos"].as_a.each do |video| - - celi=0 - - begin - printf( format, - *fcell( video["url"].as_s[12..] ), - *fcell( video["created_at"].as_s[0..15] ), - *fcell( video["broadcast_type"].as_s[0].to_s ), - *fcell( video["length"].as_i.to_s ), - *fcell( video["views"].as_i.to_s ), - *fcell( video["game"].as_s ), - *fcell( video["title"].to_s.chomp ), - *fcell( video["description"].to_s.chomp ), - ) - rescue ex - puts ex.message - pp video - end - +) +#pp celw +# if video["title"].as_s.chomp.bytesize > celw[celi] +# celw[celi] = video["title"].as_s.chomp.bytesize +# end +# if celw.sum( foh ) > winsz +# # don't exceed winsz, even on screwball wide characters +# if celi == 0 +# # preserve integrity of primary index at expense of last cell +# celw[-1] = celw[-1] - ( celw.sum( foh ) - winsz ) # 33 - 13 = 20 +# elsif celi != ( celw.size - 1 ) +# puts +# pp foh # 5 +# # sum = 93 +# pp winsz # 80 +# pp celi # 5 +# pp celw # [39, 16, 1, 8, 1, 33] +# pp celw.size # 6 +# celw[( celw.size-1 )] +# else +# celw[celi] = celw[celi] - ( celw.sum( foh ) - winsz ) - ( video["title"].as_s.chomp.bytesize - video["title"].as_s.chomp.size ) +# end +# end +# celi+=1 +# printf( "%*.*s\n", celw[celi-1], celw[celi-1], video["title"].as_s.chomp ) +# +# ) end @@ -544,7 +528,49 @@ def list_streams( settings, response ) end -def list_streams_followed( settings, client ) +def list_subs( settings, client, user_id ) + + user_id = client.user( user_id ).id + + winsz = get_winsz() + + # cell widths + celw=[0,0,0] + # cell index + celi=0 + + format = "%-*.*s %-*.*s %-*.*s\n" + + # format overhead + foh = format.delete( "^ " ).size + + response = JSON.parse( client.get_subs( user_id ) ) + + pp response + +# response["data"].as_a.sort{ |a, b| b["viewer_count"].as_i <=> a["viewer_count"].as_i }.each do |stream| + response["data"].as_a.each do |stream| + + celi=0 + + begin + printf( format, + *fcell( stream["plan_name"].as_s[0].to_s ), + *fcell( stream["gifter_login"].as_s ), + *fcell( stream["user_login"].to_s ), + ) + rescue ex + pp ex + pp stream + end + + end + puts response["points"] + +end +def list_streams_followed( settings, client, user_id ) + + user_id = client.user( user_id ).id winsz = get_winsz() @@ -558,22 +584,25 @@ def list_streams_followed( settings, client ) # format overhead foh = format.delete( "^ " ).size - response = JSON.parse( client.get_streams_followed() ) + response = JSON.parse( client.get_streams_followed( user_id ) ) pp response - response["streams"].as_a.sort{ |a, b| b["viewers"].as_i <=> a["viewers"].as_i }.each do |stream| +# response["data"].as_a.sort{ |a, b| b["viewer_count"].as_i <=> a["viewer_count"].as_i }.each do |stream| + response["data"].as_a.each do |stream| celi=0 begin printf( format, - *fcell( stream["channel"]["url"].as_s[12..] ), - *fcell( stream["stream_type"].as_s[0].to_s ), - *fcell( stream["viewers"].as_i.to_s ), - *fcell( stream["channel"]["followers"].as_i.to_s ), - *fcell( stream["game"].as_s ), - *fcell( stream["channel"]["status"].to_s ), + #*fcell( stream["user_login"].as_s[12..] ), + *fcell( "twitch.tv/#{ stream["user_login"].as_s }" ), + *fcell( stream["type"].as_s[0].to_s ), + *fcell( stream["viewer_count"].as_i.to_s ), + #*fcell( stream["channel"]["followers"].as_i.to_s ), + *fcell( "0" ), + *fcell( stream["game_name"].as_s ), + *fcell( stream["title"].to_s ), ) rescue ex pp ex @@ -586,6 +615,8 @@ end def update_channel( settings, client, id ) - pp JSON.parse( client.put_channel!( id, game: ARGV[1], status: ARGV[2] ) ) + response = client.put_channel!( id, game: ARGV[1], title: ARGV[2] ) + pp response + pp JSON.parse( client.get_channel( settings["channel_id"].to_u64 ) ) end |