commit 9e3e7b539785a09da955052f4212064fbc414a4a Author: Fijxu Date: Fri May 17 16:16:37 2024 -0400 Init diff --git a/shard.lock b/shard.lock new file mode 100644 index 0000000..0805cb9 --- /dev/null +++ b/shard.lock @@ -0,0 +1,18 @@ +version: 2.0 +shards: + backtracer: + git: https://github.com/sija/backtracer.cr.git + version: 1.2.2 + + exception_page: + git: https://github.com/crystal-loot/exception_page.git + version: 0.4.1 + + kemal: + git: https://github.com/kemalcr/kemal.git + version: 1.5.0 + + radix: + git: https://github.com/luislavena/radix.git + version: 0.4.1 + diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..3974d41 --- /dev/null +++ b/shard.yml @@ -0,0 +1,28 @@ +name: ivr-api +version: 0.1.0 + +targets: + invidious: + main: src/main.cr + +dependencies: + kemal: + github: kemalcr/kemal + + +# authors: +# - name + +# description: | +# Short description of ivr-api + +# dependencies: +# pg: +# github: will/crystal-pg +# version: "~> 0.5" + +# development_dependencies: +# webmock: +# github: manastech/webmock.cr + +# license: MIT diff --git a/src/api/users.cr b/src/api/users.cr new file mode 100644 index 0000000..d7de06e --- /dev/null +++ b/src/api/users.cr @@ -0,0 +1,61 @@ +require "json" +require "../twitchapi/*" + +module Users + def self.parseData(params) + helixUsers = JSON.parse(HelixAPI.users(params)) + login = helixUsers["data"][0]["login"] + id = helixUsers["data"][0]["id"] + # gqlUser = JSON.parse(GqlAPI.user(idto)) + gqlUser = GqlAPI.user(id.to_s) + gqlChannel = GqlAPI.channel(id.to_s) + + helixChatColor = JSON.parse(HelixAPI.chatColor(id)) + # gqlCA = JSON.parse(GqlAPI.gqlReq("ChannelAvatar", "#{login}")) + # gqlCS = JSON.parse(GqlAPI.gqlReq("ChannelShell", "#{login}")) + + # gqlUser["chatSettings"]["rules"].to_json.each do |rule| + # puts rule + # end + + json_data = [ + { + "banned" => false, + "displayName" => helixUsers["data"][0]["display_name"], + "login" => helixUsers["data"][0]["login"], + "id" => helixUsers["data"][0]["id"], + "bio" => helixUsers["data"][0]["description"], + # "followers" => gqlUser[0]["data"]["user"]["followers"]["totalCount"], + "profileViewCount" => nil, # Always null + "panelCount" => "", + "chatColor" => helixChatColor["data"][0]["color"], + "logo" => gqlUser["profileImageURL"], + "banner" => gqlUser["bannerImageURL"], + "verifiedBot" => nil, # Deprecated by twitch + "createdAt" => helixUsers["data"][0]["created_at"], + "updatedAt" => nil, + "emotePrefix" => "lol", + "chatterCount" => gqlChannel["chatters"]["count"], + "roles" => { + "isAffiliate" => gqlUser["roles"]["isAffiliate"], + "isPartner" => gqlUser["roles"]["isPartner"], + "isStaff" => gqlUser["roles"]["isStaff"], + }, + "badges" => [ + { + "setID" => "game-developer", + "title" => "Game Developer", + "description" => "Game Developer for:", + "version" => "1", + }, + ], + + "chatSettings" => { gqlUser["chatSettings"] }, + "stream" => gqlUser["stream"], + "lastBroadcast" => gqlUser["lastBroadcast"] + }, + ] + + return json_data.to_json + end +end diff --git a/src/config.cr b/src/config.cr new file mode 100644 index 0000000..e174dcc --- /dev/null +++ b/src/config.cr @@ -0,0 +1,39 @@ +require "yaml" + +class Config + include YAML::Serializable + + property oauthToken : String? + property clientID : String? + property apiEndpoint : String? + property gqlEndpoint : String? + + def self.load + config_file = "config/config.yml" + config_yaml = File.read(config_file) + config = Config.from_yaml(config_yaml) + + if config.oauthToken.to_s.empty? + puts "Config: 'oauthToken' is required/can't be empty" + exit(1) + end + + if config.clientID.to_s.empty? + puts "Config: 'oauthToken' is required/can't be empty" + exit(1) + end + + if config.apiEndpoint.to_s.empty? + puts "Config: 'apiEndpoint' is required/can't be empty" + exit(1) + end + + if config.gqlEndpoint.to_s.empty? + puts "Config: 'apiEndpoint' is required/can't be empty" + exit(1) + end + + return config + end +end + diff --git a/src/main.cr b/src/main.cr new file mode 100644 index 0000000..df94cd5 --- /dev/null +++ b/src/main.cr @@ -0,0 +1,49 @@ +require "http/server" +require "kemal" +require "json" +require "uri" +require "./config" +require "./api/*" +require "./twitchapi/*" + +CONFIG = Config.load +# puts("oauthToken: #{CONFIG.oauthToken}") +# puts("clientID: #{CONFIG.clientID}") + +before_all "/twitch/*" do |env| + env.response.content_type = "application/json" +end + +get "/twitch/user" do |env| + query = env.request.query + if query + params = URI::Params.parse(query) + if params.has_key?("login") + begin + Users.parseData(params) + rescue ex + env.response.status_code = 401 + err = { "error" => "#{ex.message}" } + err.to_json + end + elsif params.has_key?("id") + begin + Users.parseData(params) + rescue ex + env.response.status_code = 401 + err = { "error" => "#{ex.message}" } + err.to_json + end + else + env.response.status_code = 401 + err = { "error" => "Parameter 'login' or 'id' is missing" } + err.to_json + end + else + env.response.status_code = 401 + err = { "error" => "No query parameters found" } + err.to_json + end +end + +Kemal.run diff --git a/src/twitchapi/gql.cr b/src/twitchapi/gql.cr new file mode 100644 index 0000000..df50e96 --- /dev/null +++ b/src/twitchapi/gql.cr @@ -0,0 +1,110 @@ +require "json" +require "uri" + +module GqlAPI + + @@headers = HTTP::Headers{ + "Content-Type" => "application/json", + # "Client-Id" => "kimne78kx3ncx6brgo4mv6wki5h1ko", # Can cause problems due to Client-Integrity + "Client-Id" => "ue6666qo983tsx6so1t0vnawi233wa", + } + + def self.gqlOperation(operation : String, login : String) + case operation + when "ChannelShell" + data = [ + { + "operationName" => "#{operation}", + "variables" => { + "login" => "#{login}", + }, + "extensions" => { + "persistedQuery" => { + "version" => 1, + "sha256Hash" => "580ab410bcd0c1ad194224957ae2241e5d252b2c5173d8e0cce9d32d5bb14efe", + }, + }, + }, + ] + when "ChannelAvatar" + data = [ + { + "operationName" => "#{operation}", + "variables" => { + "channelLogin" => "#{login}", + }, + "extensions" => { + "persistedQuery" => { + "version" => 1, + "sha256Hash" => "84ed918aaa9aaf930e58ac81733f552abeef8ac26c0117746865428a7e5c8ab0", + }, + }, + }, + ] + end + return data.to_json + end + + def self.gqlOperation2() + data = { "query" => "{}" } + return data.to_json + end + + def self.user(id : String) + data = { "query" => "{user(id:#{id}){ + bannerImageURL, + profileImageURL(width: 600), + roles{isStaff,isAffiliate,isPartner,isExtensionsDeveloper}, + chatSettings{blockLinks,chatDelayMs,slowModeDurationSeconds,followersOnlyDurationMinutes,isBroadcasterLanguageModeEnabled,isEmoteOnlyModeEnabled,isFastSubsModeEnabled,isSubscribersOnlyModeEnabled,isUniqueChatModeEnabled,requireVerifiedAccount,rules}, + stream{averageFPS,bitrate,codec,createdAt,width,height,id,viewersCount,type,game{displayName}}, + lastBroadcast{game{displayName},id,startedAt,title} + }}" } + return JSON.parse(gqlReq(id, data.to_json))["data"]["user"] + end + + def self.channel(id : String) + data = { "query" => "{channel(id:#{id}){ + chatters{count,moderators{login},vips{login}} + }}" } + return JSON.parse(gqlReq(id, data.to_json))["data"]["channel"] + end + + def self.gqlReq(id : String, data : String) + response = HTTP::Client.post(CONFIG.gqlEndpoint.to_s, headers: @@headers, body: data) + + if response.success? + return (response.body) + else + raise "GQL Twitch API returned #{response.status_code}: #{response.body.to_s}" + end + end + + # def self.gqlReq(operation : String, login : String) + # gqlRequest = gqlOperation2() + # puts gqlRequest + + # response = HTTP::Client.post(CONFIG.gqlEndpoint.to_s, headers: @@headers, body: gqlRequest) + + # if response.success? + # return (response.body) + # else + # raise "GQL Twitch API returned #{response.status_code}: #{response.body.to_s}" + # end + # end +end + +{ + "4": { + "extensions": { + "persistedQuery": { + "sha256Hash": "84ed918aaa9aaf930e58ac81733f552abeef8ac26c0117746865428a7e5c8ab0", + "version": 1 + } + }, + "operationName": "ChannelAvatar", + "variables": { + "channelLogin": "fijxu" + } + } +} + diff --git a/src/twitchapi/helix.cr b/src/twitchapi/helix.cr new file mode 100644 index 0000000..d1977bf --- /dev/null +++ b/src/twitchapi/helix.cr @@ -0,0 +1,35 @@ +module HelixAPI + @@headers = HTTP::Headers{ + "Content-Type" => "application/json", + "Authorization" => "Bearer #{CONFIG.oauthToken}", + "Client-Id" => "#{CONFIG.clientID}", + } + + def self.users(params) + if params.has_key?("login") + endpoint = "#{CONFIG.apiEndpoint}/users?login=#{params["login"]}" + else + endpoint = "#{CONFIG.apiEndpoint}/users?id=#{params["id"]}" + end + + response = HTTP::Client.get(endpoint, headers: @@headers) + + if response.success? + return (response.body) + else + raise "Helix Twitch API returned #{response.status_code}: #{response.body.to_s}" + end + end + + def self.chatColor(id) + endpoint = "#{CONFIG.apiEndpoint}/chat/color?user_id=#{id}" + + response = HTTP::Client.get(endpoint, headers: @@headers) + + if response.success? + return (response.body) + else + raise "Helix Twitch API returned #{response.status_code}: #{response.body.to_s}" + end + end +end