From 0ce7fd4bf61d861a96d20eee65aa07615929b9cb Mon Sep 17 00:00:00 2001 From: Joe Rayhawk Date: Sat, 24 Oct 2020 23:38:06 -0700 Subject: Extensive expansions to REST and client interfaces These are major API changes that will break anything previously relying on this module. --- src/twitcr/client.cr | 60 +++++++++++++++++-- src/twitcr/rest.cr | 159 ++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 185 insertions(+), 34 deletions(-) (limited to 'src') diff --git a/src/twitcr/client.cr b/src/twitcr/client.cr index 51dedad..1a02a40 100644 --- a/src/twitcr/client.cr +++ b/src/twitcr/client.cr @@ -2,7 +2,6 @@ require "./rest" require "./mappings/*" class Twitcr::Client - include REST getter token : String? getter client_id : String @@ -10,19 +9,68 @@ class Twitcr::Client def initialize(@client_secret, @client_id) @token = nil + + include REST + + def initialize( @settings : Hash( String, String ) ) end - def user?(name : String) - true if get_user_by_login(name) + def user?( name : String ) + true if parse_user(name) rescue EMPTY_RESULT false end + # FIXME: move into a class def user(name : String) - get_user_by_login(name) + parse_user( name ) + end + + def user(id : UInt64 ) + parse_user( id ) + end + + def user_id( name : String ) + name.to_u64{ user(name).id } + end + + def user_id( id : UInt64 ) + + end + + # FIXME: move into a class + def game ( name : String ) + parse_game( name ) end - def user(id : Int64) - get_user_by_id(id) + def game ( id : UInt64 ) + parse_game( id ) end + + def game_id( name : String ) + name.to_u64{ game(name).id } + end + + def game_id( id : UInt64 ) + + end + + def unfollow( unfollower : ( String | UInt64 ), unfollowee : ( String | UInt64 ) ) + unfollower_id = user_id(unfollower) + unfollowee_id = user_id(unfollowee) + return unfollow!( unfollower_id, unfollowee_id ) + end + + def follow( follower : ( String | UInt64 ), followee : ( String | UInt64 ) ) + follower_id = user_id(follower) + followee_id = user_id(followee) + return follow!( follower_id, followee_id ) + end + + def friend( friender : ( String | UInt64 ), friendee : ( String | UInt64 ) ) + friender_id = user_id(friender) + friendee_id = user_id(friendee) + return friend!( friender_id, friendee_id ) + end + end diff --git a/src/twitcr/rest.cr b/src/twitcr/rest.cr index cb2c1a0..f3af85c 100644 --- a/src/twitcr/rest.cr +++ b/src/twitcr/rest.cr @@ -1,65 +1,168 @@ require "http" +require "uri" require "json" require "./mappings/*" module Twitcr::REST # Mixin for interacting with Twitch's REST API SSL_CONTEXT = OpenSSL::SSL::Context::Client.new - API_BASE = "https://api.twitch.tv/helix" + API_BASE = "https://api.twitch.tv/" EMPTY_RESULT = Exception.new("Empty Result") - # Executes an HTTP request against the API_BASE url - def request(method : String, route : String, version = "5", headers = HTTP::Headers.new, body : String? = nil) - get_token unless @token + enum Api + Api # typically undocumented APIs go here + Kraken # v5, "deprecated" with no real replacement for half of the functions + Helix # "new api" + PubSub # FIXME: put this in another module, probably + end + + # Execute HTTP request + def request( method : String, twitchapi : Api, route : String, body : String? = nil, *, contenttype : String? = nil ) + + headers = HTTP::Headers.new + headers["Client-ID"] = @settings["client_id"].not_nil! + + api = twitchapi.to_s.downcase + case api + when "helix" + # FIXME: maybe don't send this every time? + headers["Authorization"] = "Bearer #{@settings["access_token"].not_nil!}" + when "kraken" + headers["Authorization"] = "OAuth #{@settings["access_token"].not_nil!}" + headers["Accept"] = "application/vnd.twitchtv.v5+json" + when "api" + # typically twitch restricts these to the twitch client id + headers["Client-ID"] = @settings["client_id_twitch"].not_nil! + else + end + + if contenttype + headers["Content-Type"] = contenttype + end - headers["Authorization"] = @token || "test" - headers["Client-ID"] = @client_id + begin + ENV["DEBUG"] + rescue KeyError + else + puts method + " " + API_BASE + api + route + #pp headers# probably too sensitive + pp body + end response = HTTP::Client.exec( method, - API_BASE + route, + API_BASE + api + route, headers, + body, tls: SSL_CONTEXT ) - if response.status_code == 401 - get_token - puts "Had to get new token" - return request(method, route, version, headers, body) + begin + ENV["DEBUG"] + rescue KeyError + else + pp response.body end response.body end - def get_user_by_login(login : String) - response = request( - "GET", - "/users?login=" + login - ) - - list = UserList.from_json(response) + # "!" denotes state mutation. "REST" is a bit of a misnomer. + def get_user( login : String ); request( "GET", Api::Helix, "/users?login=#{login}" ) end + def get_user( id : UInt64 ); request( "GET", Api::Helix, "/users?id=#{id.to_s}" ) end + def get_user( id : Array( UInt64 ) ); request( "GET", Api::Helix, "/users?id=#{id.map { |name| name.to_s }.join("&id=")}" ) end + def get_user( id : Array( String ) ); request( "GET", Api::Helix, "/users?login=#{login.join("&login=")}" ) end + def follow!( from : UInt64, to : UInt64 ); request( "PUT", Api::Kraken, "/users/#{from.to_s}/follows/channels/#{to.to_s}" ) end + def unfollow!( from : UInt64, to : UInt64 ); request( "DELETE", Api::Kraken, "/users/#{from.to_s}/follows/channels/#{to.to_s}" ) end + def get_user_follows( *, from : UInt64, after : String? = nil ) + params = Hash( String, String ).new + params["first"] = "100" + params["from_id"] = from.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, "/users/follows" + query_string ) + end + def get_user_follows( *, to : UInt64, after : String? = nil ) + params = Hash( String, String ).new + params["first"] = "100" + params["to_id"] = to.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, "/users/follows" + query_string ) + end + # FIXME: support array of mixed UInt64/String + def get_games( name : String ); request( "GET", Api::Helix, "/games?name=#{URI.encode_www_form( name )}" ) end + def get_games( id : UInt64 ); request( "GET", Api::Helix, "/games?id=#{id.to_s}" ) end + def get_streams( user_ids : Array( UInt64 ) ); request( "GET", Api::Helix, "/streams?user_id=#{user_ids.join( "&user_id=" )}" ) end + def get_streams( user_id : UInt64 ); request( "GET", Api::Helix, "/streams?user_id=#{user_id}" ) end + def get_streams( game_id : UInt64 ); request( "GET", Api::Helix, "/streams?first=100&game_id=#{game_id}" ) end + # Kraken returns more information + def get_streams_v5( game : String ); request( "GET", Api::Kraken, "/streams?limit=100&game=#{URI.encode_www_form( game )}" ) end + def get_streams_v5( *, channel : ( Array( UInt64 ) | UInt64 | Nil ) = nil, game : String? = nil, language : String? = nil, stream_type : String? = nil ); + params = Hash( String, String ).new + case channel + when Nil + when UInt64 + params["channel"] = channel.to_s + when Array( UInt64 ) + params["channel"] = channel.map { |id| id.to_s }.join(",") + else + end + if game ; params["game"] = game end + if language ; params["language"] = language end + if stream_type ; params["stream_type"] = stream_type end + params["limit"] = "100" + query_string = HTTP::Params.encode( params ) + 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 + # 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_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 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 + + # 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 + # https://github.com/justintv/Twitch-API/issues/545 requires weird scopes that normal client_ids can't get + def friend!( friender : UInt64, friendee : UInt64 ); request( "PUT", Api::Kraken, "/users/#{friender.to_s}/friends/#{friendee.to_s}" ) end + 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 ) + list = GameList.from_json( get_games( game ) ) raise EMPTY_RESULT if list.data.empty? - list.data.first end - def get_user_by_id(id : Int64) - response = request( - "GET", - "/users?id=" + id.to_s - ) - - list = UserList.from_json(response) + def parse_user( user : UInt64 | String ) + list = UserList.from_json( get_user( user ) ) raise EMPTY_RESULT if list.data.empty? - list.data.first end def get_token response = HTTP::Client.exec( "POST", - "https://id.twitch.tv/oauth2/token?client_id=#{@client_id}&client_secret=#{@client_secret}&grant_type=client_credentials", + "https://id.twitch.tv/oauth2/token?client_id=#{@settings["client_id"].not_nil!}&client_secret=#{@settings["client_secret"].not_nil!}&grant_type=client_credentials", tls: SSL_CONTEXT ) -- cgit v1.2.3