This commit is contained in:
Fijxu 2024-06-22 02:35:49 -04:00
parent 7fbc39436b
commit c085b71ede
Signed by: Fijxu
GPG key ID: 32C1DDF333EDA6A4
17 changed files with 226065 additions and 379 deletions

11
.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
# Binaries
bin
# User config
config/config.yml
# libs
lib
# Syncthing
.stfolder

View file

@ -1,16 +1,24 @@
## How to run
- Clone repo
- Copy the `config/config.example.yml` to `config/config.yml`
- Fill every option
- Compile using `shards build --release`
- Run `./bin/ivr-api-crystal`
### TODO ### TODO
- ~Use Redis to cache replies and add a new key to report for how much time the JSON is going to be stored in cache (**Already started, but I need to fix it!!**)~ (Only done for the user endpoint) - ~~Use Redis to cache replies and add a new key to report for how much time the JSON is going to be stored in cache~~ Done.
- ~Add other endpoints from api.ivr.fi~ (Almost everything) - ~~Add other endpoints from api.ivr.fi~~ Almost done.
- CAPTURE MORE THAN 1 PARAM `?login=fuck,fuck` - ~~Capture more than 1 param: `?login=fijxu,fijxu`~~ Done.
### NOT SO IMPORTANT TODO ### NOT SO IMPORTANT TODO
- Rate limiting (can be done in the reverse proxy side) - ~~STOP USING KEMAL FOR SIMPLE THINGS~~ Nevermind, kemal is pretty useful and it should stay
- ~Better shitcode~ Better goodcode - `programAgreement { type }` in the Next commit.
- ~STOP USING KEMAL FOR SIMPLE THINGS!~ Nevermind, kemal is pretty useful and it should stay - Guide on how to get the tokens to get it working
- Kick API
### Suggestions ### Suggestions
3:45 RyanPotat: add team ~~3:45 RyanPotat: add team~~ Next commit.

View file

@ -1,10 +1,10 @@
# Please fill everything # Please fill everything
helixOAuth: "" helixOAuth: ""
helixClientID: "" helixClientID: ""
gqlOAuth: "" gqlOAuth: ""
gqlClientID: "" gqlClientID: ""
apiEndpoint: "https://api.twitch.tv/helix" apiEndpoint: "https://api.twitch.tv/helix"
gqlEndpoint: "https://gql.twitch.tv/gql" gqlEndpoint: "https://gql.twitch.tv/gql"
redis_url: "tcp://127.0.0.1:6379" redis_addr: "127.0.0.1:6379"
port: 8080 redis_database: 1
port: 8081

47398
schema.graphql Normal file

File diff suppressed because it is too large Load diff

178032
schema.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,10 @@ shards:
git: https://github.com/sija/backtracer.cr.git git: https://github.com/sija/backtracer.cr.git
version: 1.2.2 version: 1.2.2
db:
git: https://github.com/crystal-lang/crystal-db.git
version: 0.13.1
exception_page: exception_page:
git: https://github.com/crystal-loot/exception_page.git git: https://github.com/crystal-loot/exception_page.git
version: 0.4.1 version: 0.4.1
@ -12,15 +16,11 @@ shards:
git: https://github.com/kemalcr/kemal.git git: https://github.com/kemalcr/kemal.git
version: 1.5.0 version: 1.5.0
pool:
git: https://github.com/ysbaddaden/pool.git
version: 0.2.4
radix: radix:
git: https://github.com/luislavena/radix.git git: https://github.com/luislavena/radix.git
version: 0.4.1 version: 0.4.1
redis: redis:
git: https://github.com/stefanwille/crystal-redis.git git: https://github.com/jgaskins/redis.git
version: 2.9.1 version: 0.9.0

View file

@ -1,30 +1,22 @@
name: ivr-api name: ivr-crystal
version: 0.1.0 version: 0.2.0
targets: targets:
invidious: ivr-api-crystal:
main: src/main.cr main: src/main.cr
dependencies: dependencies:
kemal: kemal:
github: kemalcr/kemal github: kemalcr/kemal
# redis:
# github: stefanwille/crystal-redis
redis: redis:
github: stefanwille/crystal-redis github: jgaskins/redis
authors:
- Fijxu
# authors: description: |
# - name <email@example.com> Drop-in api.ivr.fi replacement made using Crystal.
# description: | license: Unlicense
# Short description of ivr-api
# dependencies:
# pg:
# github: will/crystal-pg
# version: "~> 0.5"
# development_dependencies:
# webmock:
# github: manastech/webmock.cr
# license: MIT

View file

@ -3,31 +3,21 @@ require "yaml"
class Config class Config
include YAML::Serializable include YAML::Serializable
property helixOAuth : String? property helixOAuth : String
property helixClientID : String? property helixClientID : String
property gqlOAuth : String? property gqlOAuth : String
property gqlClientID : String? property gqlClientID : String
property apiEndpoint : String? property apiEndpoint : String? = "https://api.twitch.tv/helix"
property gqlEndpoint : String? property gqlEndpoint : String? = "https://gql.twitch.tv/gql"
property redis_url : String? property redis_addr : String? = "127.0.0.1:6379"
property redis_socket : String? property redis_socket : String?
property redis_database : Int32?
property port : Int32 = 8080 property port : Int32 = 8080
def self.load def self.load
config_file = "config/config.yml" config_file = "config/config.yml"
config_yaml = File.read(config_file) config_yaml = File.read(config_file)
config = Config.from_yaml(config_yaml) config = Config.from_yaml(config_yaml)
if config.helixOAuth.to_s.empty?
puts "Config: 'helixOAuth' is required/can't be empty"
exit(1)
end
if config.helixClientID.to_s.empty?
puts "Config: 'helixOAuth' is required/can't be empty"
exit(1)
end
return config return config
end end
end end

View file

@ -1,159 +1,215 @@
module TwAPI::Routes::Twitch module TwAPI::Datahandlers::Twitch
extend self extend self
def checkBanned(gql) private macro error(message)
if GqlAPI.urb.includes? "login" env.response.content_type = "application/json"
if gql["userResultByLogin"]["reason"]? env.response.status_code = 403
return gql["userResultByLogin"]["reason"] error_message = {"error" => {{message}}, "twitchResponse" => gql}.to_json
else return error_message
return nil end
end
elsif GqlAPI.urb.includes? "id" private macro twitchError
if gql["userResultByID"]["reason"]? env.response.content_type = "application/json"
return gql["userResultByID"]["reason"] env.response.status_code = 403
else error_message = {"error" => ex.message, "twitchResponse" => gql}.to_json
return nil return error_message
end end
private macro skipCache
env.params.query["skipCache"]?
end
def isBannedLogin(gql)
if gql["userResultByLogin"]["reason"]?
return true
else
return false
end end
end end
def parseData(params) def isBannedId(gql)
if !params.has_key?("login") && !params.has_key?("id") if gql["userResultByID"]["reason"]?
err = {"error" => "No query parameters found"} return true
return err.to_json
end
login = params["login"]?
id = params["id"]?
if login && (json = REDIS_DB.get(login))
# puts(JSON.parse(json))
return json
elsif id && (json = REDIS_DB.get(id))
return json
end
# puts REDIS_DB.get(params["login"]?)
# puts REDIS_DB.get(params["id"]?)
# if (json = (REDIS_DB.get(params["login"]?.to_s))) || (json = (REDIS_DB.get(params["id"]?.to_s)))
# if ((json = REDIS_DB.get(id))
# puts json
# return json
# end
gql = GqlAPI.queryUser(params)
if (gql["user"] == nil)
err = {"message" => "Specified user does not exist"}
return err.to_json
end
# puts gql
panels = gql["user"]["panels"]
banned = checkBanned(gql)
# puts(banned)
# Using JSON::Builder will be better I guess
json_data = [
{
"cache" => {
"isCached" => false,
"expireTime" => nil : Int32,
},
"banned" => false,
"reason" => banned,
"displayName" => gql["user"]["displayName"],
"login" => gql["user"]["login"],
"id" => gql["user"]["id"],
"bio" => gql["user"]["description"],
"follows" => nil,
"followers" => gql["user"]["followers"]["totalCount"],
"profileViewCount" => nil, # Always null
"panelCount" => panels.size,
"chatColor" => gql["user"]["chatColor"],
"logo" => gql["user"]["profileImageURL"],
"banner" => gql["user"]["bannerImageURL"],
"verifiedBot" => nil, # Deprecated by twitch
"createdAt" => gql["user"]["createdAt"],
"updatedAt" => gql["user"]["updatedAt"],
"deletedAt" => gql["user"]["deletedAt"],
"emotePrefix" => gql["user"]["emoticonPrefix"]["name"],
"roles" => {
"isAffiliate" => gql["user"]["roles"]["isAffiliate"],
"isPartner" => gql["user"]["roles"]["isPartner"],
"isStaff" => gql["user"]["roles"]["isStaff"],
},
"badges" => gql["user"]["displayBadges"],
"chatterCount" => gql["user"]["channel"]["chatters"]["count"],
"chatSettings" => gql["user"]["chatSettings"],
"stream" => gql["user"]["stream"],
"lastBroadcast" => gql["user"]["lastBroadcast"],
"panels" => panels,
},
]
if banned != nil
json_data[0]["banned"] = true
end
if id
REDIS_DB.set(id, json_data.to_json, ex: 5)
else else
REDIS_DB.set(login.to_s, json_data.to_json, ex: 5) return false
end end
return json_data.to_json
end end
def user(env) def banReason(gql)
# TODO: Clean this mess shit of else if else if else if if gql["userResultByLogin"] == nil
if env.params.query return false
params = env.params.query
if params.has_key?("login") || params.has_key?("id")
begin
parseData(params)
rescue ex
env.response.status_code = 404
err = {"error" => "#{ex.message}"}
err.to_json
end
elsif params.has_key?("id")
begin
parseData(params)
rescue ex
env.response.status_code = 404
err = {"error" => "#{ex.message}"}
err.to_json
end
else
env.response.status_code = 404
err = {"error" => "Parameter 'login' or 'id' is missing"}
err.to_json
end
else
env.response.status_code = 404
err = {"error" => "No query parameters found"}
err.to_json
end end
return true
end
def userLogin(env)
if skipCache != "true"
if (json = Utils::Redis.retrieveFromCache("user-login_#{env.params.query["login"].try &.split(',')}"))
json = JSON.parse(json)
return json.to_json
end
end
begin
gql = GqlAPI.getUserByLogin(env.params.query)
rescue ex
twitchError
end
json_data = JSON.build do |json|
json.array do
gql.each do |gql|
gql = gql["data"]
if (gql["user"] == nil)
# v3 could add some feedback error message instead of an empty array.
nil # Do not add anything to the array. Just like api.ivr.fi does.
else
panels = gql["user"]["panels"]
banned = isBannedLogin(gql)
json.object do
json.field "banned", banned
if banned == true
json.field "banReason", gql["userResultByLogin"]["reason"]
end
json.field "displayName", gql["user"]["displayName"]
json.field "login", gql["user"]["login"]
json.field "id", gql["user"]["id"]
json.field "bio", gql["user"]["description"]
json.field "follows", nil
json.field "followers", gql["user"]["followers"]["totalCount"]
json.field "profileViewCount", nil # Always null
json.field "panelCount", panels.size
json.field "chatColor", gql["user"]["chatColor"]
json.field "logo", gql["user"]["profileImageURL"]
json.field "banner", gql["user"]["bannerImageURL"]
json.field "verifiedBot", nil # Deprecated by twitch
json.field "createdAt", gql["user"]["createdAt"]
json.field "updatedAt", gql["user"]["updatedAt"]
json.field "deletedAt", gql["user"]["deletedAt"]
json.field "emotePrefix", gql["user"]["emoticonPrefix"]["name"]
json.field "roles", gql["user"]["roles"]
json.field "badges", gql["user"]["displayBadges"]
json.field "chatterCount", gql["user"]["channel"]["chatters"]["count"]
json.field "chatSettings", gql["user"]["chatSettings"]
json.field "stream", gql["user"]["stream"]
json.field "lastBroadcast", gql["user"]["lastBroadcast"]
json.field "panels", panels
end
end
end
end
end
begin
Utils::Redis.saveJson("user-login_#{env.params.query["login"].try &.split(',')}", json_data)
rescue ex
puts ex.message
end
return json_data
end
def userId(env)
if skipCache != "true"
if (json = Utils::Redis.retrieveFromCache("user-id_#{env.params.query["id"].try &.split(',')}"))
json = JSON.parse(json)
return json.to_json
end
end
begin
gql = GqlAPI.getUserById(env.params.query)
rescue ex
twitchError
end
json_data = JSON.build do |json|
json.array do
gql.each do |gql|
gql = gql["data"]
if (gql["user"] == nil)
# v3 could add some feedback error message instead of an empty array.
# Do not add anything to the array. Just like api.ivr.fi does.
nil
else
panels = gql["user"]["panels"]
banned = isBannedId(gql)
json.object do
json.field "banned", banned
if banned == true
json.field "banReason", gql["userResultByID"]["reason"]
end
json.field "displayName", gql["user"]["displayName"]
json.field "login", gql["user"]["login"]
json.field "id", gql["user"]["id"]
json.field "bio", gql["user"]["description"]
json.field "follows", nil
json.field "followers", gql["user"]["followers"]["totalCount"]
json.field "profileViewCount", nil # Always null
json.field "panelCount", panels.size
json.field "chatColor", gql["user"]["chatColor"]
json.field "logo", gql["user"]["profileImageURL"]
json.field "banner", gql["user"]["bannerImageURL"]
json.field "verifiedBot", nil # Deprecated by twitch
json.field "createdAt", gql["user"]["createdAt"]
json.field "updatedAt", gql["user"]["updatedAt"]
json.field "deletedAt", gql["user"]["deletedAt"]
json.field "emotePrefix", gql["user"]["emoticonPrefix"]["name"]
json.field "roles", gql["user"]["roles"]
json.field "badges", gql["user"]["displayBadges"]
json.field "chatterCount", gql["user"]["channel"]["chatters"]["count"]
json.field "chatSettings", gql["user"]["chatSettings"]
json.field "stream", gql["user"]["stream"]
json.field "lastBroadcast", gql["user"]["lastBroadcast"]
json.field "panels", panels
end
end
end
end
end
begin
Utils::Redis.saveJson("user-id_#{env.params.query["id"].try &.split(',')}", json_data)
rescue ex
puts ex.message
end
return json_data
end end
def modvip(env) def modvip(env)
# if !env.params.query.has_key?("login") && !env.params.query.has_key?("id") channel = env.params.url["channel"]
# err = {"error" => "No query parameters found"}
# return err.to_json if (json = Utils::Redis.retrieveFromCache("modvip-#{channel}")) && skipCache != "true"
# else json = JSON.parse(json)
if !env.params.url.has_key?("channel") json.as_h["cache"].as_h["isCached"] = JSON::Any.new(true)
err = {"error" => "Please insert a channel"} json.as_h["cache"].as_h["expireTime"] = JSON::Any.new(Utils::Redis.getExpireTime("modvip-#{channel}"))
return err.to_json return json.to_json
end end
gql = GqlAPI.getModsVips(env.params.url)["user"] begin
gql = GqlAPI.getModsVips(channel)
gql = gql["data"]["user"]
rescue ex
twitchError
end
if (gql == nil) if (gql == nil)
err = {"message" => "Specified user does not exist"} error("Specified channel does no exist")
return err.to_json
end end
json_data = JSON.build do |json| json_data = JSON.build do |json|
json.object do json.object do
json.field "cache" do
json.object do
json.field "isCached", false
json.field "expireTime", nil
end
end
json.field "modCount", gql["mods"]["edges"].size json.field "modCount", gql["mods"]["edges"].size
json.field "vipCount", gql["vips"]["edges"].size json.field "vipCount", gql["vips"]["edges"].size
json.field "mods" do json.field "mods" do
@ -196,29 +252,43 @@ module TwAPI::Routes::Twitch
end end
end end
end end
Utils::Redis.saveJson("modvip-#{channel}", json_data)
return json_data return json_data
end end
# TODO: Add error message if query doesn't exist at all # TODO: Add error message if query doesn't exist at all
def founders(env) def founders(env)
# if !env.params.query.has_key?("login") && !env.params.query.has_key?("id") login = env.params.url["login"]
# err = {"error" => "No query parameters found"}
# return err.to_json
# else
if !env.params.url.has_key?("login")
err = {"error" => "Please insert a channel"}
return err.to_json
end
gql = GqlAPI.getFounders(env.params.url)["user"]
if (json = Utils::Redis.retrieveFromCache("founders-#{login}")) && skipCache != "true"
json = JSON.parse(json)
json.as_h["cache"].as_h["isCached"] = JSON::Any.new(true)
json.as_h["cache"].as_h["expireTime"] = JSON::Any.new(Utils::Redis.getExpireTime("founders-#{login}"))
return json.to_json
end
begin
gql = GqlAPI.getFounders(login)
gql = gql["data"]["user"]
rescue ex
twitchError
end
# TODO: use begin rescue for this too!
if (gql == nil) if (gql == nil)
err = {"message" => "Specified user does not exist"} error("Specified channel does no exist")
return err.to_json
end end
json_data = JSON.build do |json| json_data = JSON.build do |json|
json.object do json.object do
json.field "cache" do
json.object do
json.field "isCached", false
json.field "expireTime", nil
end
end
json.field "foundersCount", gql["channel"]["founders"].size json.field "foundersCount", gql["channel"]["founders"].size
json.field "founders" do json.field "founders" do
json.array do json.array do
@ -242,27 +312,41 @@ module TwAPI::Routes::Twitch
end end
end end
end end
Utils::Redis.saveJson("founders-#{login}", json_data)
return json_data
end end
def clip(env) def clip(env)
# if !env.params.query.has_key?("slug") slug = env.params.url["slug"]
# err = {"error" => "No query parameters found"}
# return err.to_jso if (json = Utils::Redis.retrieveFromCache("clip-#{slug}")) && skipCache != "true"
# else json = JSON.parse(json)
if !env.params.url.has_key?("slug") json.as_h["cache"].as_h["isCached"] = JSON::Any.new(true)
err = {"error" => "Please insert a slug"} json.as_h["cache"].as_h["expireTime"] = JSON::Any.new(Utils::Redis.getExpireTime("clip-#{slug}"))
return err.to_json return json.to_json
end end
gql = GqlAPI.getClips(env.params.url)["clip"] begin
gql = GqlAPI.getClips(slug)
gql = gql["data"]["clip"]
rescue ex
twitchError
end
# TODO: use begin rescue for this too! (again)
if (gql == nil) if (gql == nil)
err = {"message" => "Specified user does not exist"} error("Slug does not exist")
return err.to_json
end end
json_data = JSON.build do |json| json_data = JSON.build do |json|
json.object do json.object do
json.field "cache" do
json.object do
json.field "isCached", false
json.field "expireTime", nil
end
end
json.field "clip" do json.field "clip" do
json.object do json.object do
json.field "durationSeconds", gql["durationSeconds"] json.field "durationSeconds", gql["durationSeconds"]
@ -321,22 +405,40 @@ module TwAPI::Routes::Twitch
end end
end end
end end
Utils::Redis.saveJson("clip-#{slug}", json_data)
return json_data
end end
def subage(env) def subage(env)
if !env.params.url.has_key?("user") || !env.params.url.has_key?("channel") user = env.params.url["user"]
err = {"error" => "No query parameters found"} channel = env.params.url["channel"]
return err.to_json
if (json = Utils::Redis.retrieveFromCache("subage-user_#{user}_channel_#{channel}")) && skipCache != "true"
json = JSON.parse(json)
json.as_h["cache"].as_h["isCached"] = JSON::Any.new(true)
json.as_h["cache"].as_h["expireTime"] = JSON::Any.new(Utils::Redis.getExpireTime("subage-user_#{user}_channel_#{channel}"))
return json.to_json
end end
gql = GqlAPI.getSubage(env.params.url["user"], env.params.url["channel"]) begin
if (gql == nil) gql = GqlAPI.getSubage(user, channel)
err = {"message" => "Specified user does not exist"} gql = gql["data"]
return err.to_json rescue ex
if (gql == nil)
error("Channel does not exist")
end
twitchError
end end
json_data = JSON.build do |json| json_data = JSON.build do |json|
json.object do json.object do
json.field "cache" do
json.object do
json.field "isCached", false
json.field "expireTime", nil
end
end
json.field "user" do json.field "user" do
json.object do json.object do
json.field "id", gql["userData"]["id"]? json.field "id", gql["userData"]["id"]?
@ -353,7 +455,7 @@ module TwAPI::Routes::Twitch
end end
json.field "statusHidden", false # TODO: what is this? I need to get it NOW! json.field "statusHidden", false # TODO: what is this? I need to get it NOW!
json.field "followedAt", gql["info"]["relationship"]["followedAt"] json.field "followedAt", gql["info"]["relationship"]["followedAt"]
if (gql["info"]["relationship"]["streak"] == nil) if (gql["info"]["relationship"]["streak"] == nil) || (gql["info"]["relationship"]["streak"]["elapsedDays"]? == 0)
json.field "streak", nil json.field "streak", nil
else else
json.field "streak" do json.field "streak" do
@ -366,7 +468,7 @@ module TwAPI::Routes::Twitch
end end
end end
end end
if (gql["info"]["relationship"]["cumulative"] == nil) if (gql["info"]["relationship"]["cumulative"] == nil) || (gql["info"]["relationship"]["cumulative"]["elapsedDays"]? == 0)
json.field "cumulative", nil json.field "cumulative", nil
else else
json.field "cumulative" do json.field "cumulative" do
@ -394,23 +496,39 @@ module TwAPI::Routes::Twitch
end end
end end
end end
Utils::Redis.saveJson("subage-user_#{user}_channel_#{channel}", json_data)
return json_data
end end
def emotes(env) def emotes(env)
if !env.params.url.has_key?("emote") emoteName = env.params.url["emote"]
err = {"error" => "No query parameters found"}
return err.to_json if (json = Utils::Redis.retrieveFromCache("emotes-#{emoteName}")) && skipCache != "true"
json = JSON.parse(json)
json.as_h["cache"].as_h["isCached"] = JSON::Any.new(true)
json.as_h["cache"].as_h["expireTime"] = JSON::Any.new(Utils::Redis.getExpireTime("emotes-#{emoteName}"))
return json.to_json
end end
gql = GqlAPI.getEmotes(env.params.url["emote"])["emote"] # TODO: support if ID or emote name begin
gql = GqlAPI.getEmotes(emoteName)
if (gql == nil) gql = gql["data"]["emote"]
err = {"message" => "Specified emote does not exist"} if gql == nil
return err.to_json raise "Emote does not exist"
end
rescue ex
twitchError
end end
json_data = JSON.build do |json| json_data = JSON.build do |json|
json.object do json.object do
json.field "cache" do
json.object do
json.field "isCached", false
json.field "expireTime", nil
end
end
json.field "channelName", gql["owner"]["displayName"] json.field "channelName", gql["owner"]["displayName"]
json.field "channelLogin", gql["owner"]["login"] json.field "channelLogin", gql["owner"]["login"]
json.field "channelID", gql["owner"]["id"] json.field "channelID", gql["owner"]["id"]
@ -424,20 +542,22 @@ module TwAPI::Routes::Twitch
json.field "emoteTier", gql["subscriptionTier"] json.field "emoteTier", gql["subscriptionTier"]
end end
end end
Utils::Redis.saveJson("emotes-#{emoteName}", json_data)
return json_data
end end
def channelEmotes(env) def channelEmotes(env)
if !env.params.query.has_key?("channel")
err = {"error" => "No query parameters found"}
return err.to_json
end
gql = GqlAPI.getChannelEmotes(env.params.query["channel"]) # TODO: Add support for IDs an not just the channel name gql = GqlAPI.getChannelEmotes(env.params.query["channel"]) # TODO: Add support for IDs an not just the channel name
if (gql == nil)
err = {"message" => "Specified user does not exist"}
return err.to_json
end
json_data = JSON.build do |json| json_data = JSON.build do |json|
json.object do json.object do
json.field "cache" do
json.object do
json.field "isCached", false
json.field "expireTime", nil
end
end
json.field "subProducts" do json.field "subProducts" do
json.array do json.array do
gql["user"]["subEmotes"]?.try &.as_a.each do |subEmotes| gql["user"]["subEmotes"]?.try &.as_a.each do |subEmotes|
@ -491,14 +611,3 @@ module TwAPI::Routes::Twitch
end end
end end
end end
# I'll do this later fuck this shit
# def globalEmotes(env)
# if !env.params.query.has_key?("id") || !env.params.query.has_key?("login")
# err = "[]"
# return err.to_json
# else
# gql = GqlAPI.getChannelEmotes(env.params.query)
# json_data = JSON.build do |json|
# end

View file

@ -6,57 +6,21 @@ require "redis"
require "./config" require "./config"
require "./routes/**" require "./routes/**"
require "./datahandlers/**"
require "./twitchapi/*" require "./twitchapi/*"
require "./utils/*"
module TwAPI module TwAPI
end end
alias TA = TwAPI CONFIG = Config.load
CONFIG = Config.load
Kemal.config.port = CONFIG.port Kemal.config.port = CONFIG.port
REDIS_DB = Redis::PooledClient.new(unixsocket: CONFIG.redis_socket || nil, url: CONFIG.redis_url || nil) REDIS_UTILS = TwAPI::Utils::Redis
if REDIS_DB.ping REDIS_DB = Redis::Client.new(URI.parse("redis://#{CONFIG.redis_addr}/#{CONFIG.redis_database}#?keepalive=true"))
puts "Connected to redis" puts "Connected to Redis"
end
before_all "/v2/*" do |env| TwAPI::Routes.register_misc
env.response.content_type = "application/json" TwAPI::Routes.register_all_twitch
end
TwAPI::Routing.register_all()
# get "/v2/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
{% if flag?(:release) || flag?(:production) %} {% if flag?(:release) || flag?(:production) %}
Kemal.config.env = "production" if !ENV.has_key?("KEMAL_ENV") Kemal.config.env = "production" if !ENV.has_key?("KEMAL_ENV")

View file

@ -1,9 +1,8 @@
module TwAPI::Routing module TwAPI::Routes
extend self # To prevent writing self. at the start of every function extend self # To prevent writing self. at the start of every function
# Thanks Invidious devs # Thanks Invidious devs
{% for http_method in {"get", "post", "delete", "options", "patch", "put"} %} {% for http_method in {"get", "post", "delete", "options", "patch", "put"} %}
macro {{http_method.id}}(path, controller, method = :handle) macro {{http_method.id}}(path, controller, method = :handle)
unless Kemal::Utils.path_starts_with_slash?(\{{path}}) unless Kemal::Utils.path_starts_with_slash?(\{{path}})
raise Kemal::Exceptions::InvalidPathStartException.new({{http_method}}, \{{path}}) raise Kemal::Exceptions::InvalidPathStartException.new({{http_method}}, \{{path}})
@ -13,11 +12,23 @@ module TwAPI::Routing
\{{ controller }}.\{{ method.id }}(env) \{{ controller }}.\{{ method.id }}(env)
end end
end end
{% end %} {% end %}
def register_all def register_misc
# get "/" before_all "/v2/*" do |env|
env.response.content_type = "application/json"
end
get "/" do |env|
env.redirect "/v2"
end
get "/v2" do
{"message": "Welcome to v2, There is no docs!"}.to_json
end
end
def register_all_twitch
get "/v2/twitch/user", TwAPI::Routes::Twitch, :user get "/v2/twitch/user", TwAPI::Routes::Twitch, :user
get "/v2/twitch/modvip/", TwAPI::Routes::Twitch, :modvip get "/v2/twitch/modvip/", TwAPI::Routes::Twitch, :modvip

52
src/routes/twitch.cr Normal file
View file

@ -0,0 +1,52 @@
module TwAPI::Routes::Twitch
extend self
private macro handleData(func)
begin
TwAPI::Datahandlers::Twitch.{{func}}(env)
rescue ex
env.response.status_code = 404
err = {"error" => "#{ex.message}"}
err.to_json
end
end
def user(env)
if env.params.query.has_key?("login")
handleData(userLogin)
else
handleData(userId)
end
end
def modvip(env)
handleData(modvip)
end
def founders(env)
handleData(founders)
end
def clip(env)
handleData(clip)
end
def subage(env)
handleData(subage)
end
def emotes(env)
handleData(emotes)
end
end
# I'll do this later fuck this shit
# def globalEmotes(env)
# if !env.params.query.has_key?("id") || !env.params.query.has_key?("login")
# err = "[]"
# return err.to_json
# else
# gql = GqlAPI.getChannelEmotes(env.params.query)
# json_data = JSON.build do |json|
# end

View file

@ -1,18 +1,6 @@
module GqlAPI module TwAPI::GqlAPI
extend self extend self
@@headers = HTTP::Headers{
"Content-Type" => "application/json",
# "Client-Id" => "kimne78kx3ncx6brgo4mv6wki5h1ko", # Can cause problems due to Client-Integrity
"Authorization" => "OAuth #{CONFIG.gqlOAuth}",
"Client-Id" => "#{CONFIG.gqlClientID}",
}
@@urb : String = "fuck"
def urb
return @@urb
end
def userResultBy(params) def userResultBy(params)
if params.has_key?("id") if params.has_key?("id")
return %(userResultByID(id: "#{params["id"]}")) return %(userResultByID(id: "#{params["id"]}"))
@ -37,27 +25,22 @@ module GqlAPI
end end
end end
def user(params) def getUserByLogin(params)
if params.has_key?("id") logins = params["login"].try &.split(',')
return %(id: "#{params["id"]}" lookupType: ALL) channel = Channel(JSON::Any).new
elsif params.has_key?("login") responses = [] of JSON::Any
return %(login: "#{params["login"]}" lookupType: ALL) queries = [] of String
else
return %(login: "#{params["channel"]}" lookupType: ALL)
end
end
def queryUser(params) logins.each do |login|
@@urb = userResultBy(params) query = %(
query = %(
query { query {
#{userResultBy(params)} { userResultByLogin(login: "#{login}") {
... on UserDoesNotExist { ... on UserDoesNotExist {
key key
reason reason
} }
} }
user(#{user(params)}) { user(login: "#{login}" lookupType: ALL) {
id id
language language
login login
@ -138,13 +121,140 @@ module GqlAPI
} }
} }
) )
return (JSON.parse(gqlReq(query)))["data"] queries << query
end
queries.each do |query|
spawn do
result = JSON.parse(req(query))
channel.send(result)
end
end
queries.each do
responses << channel.receive
end
return responses
end end
def getModsVips(env) def getUserById(params)
ids = params["id"].try &.split(',')
channel = Channel(JSON::Any).new
responses = [] of JSON::Any
queries = [] of String
ids.each do |id|
query = %(
query {
userResultByID(id: "#{id}") {
... on UserDoesNotExist {
key
reason
}
}
user(id: "#{id}" lookupType: ALL) {
id
language
login
displayName
description
bannerImageURL
profileImageURL(width: 600)
createdAt
updatedAt
deletedAt
chatColor
emoticonPrefix {
name
}
panels(hideExtensions: false) {
id
type
}
followers {
totalCount
}
roles {
isStaff
isAffiliate
isPartner
isExtensionsDeveloper
}
displayBadges {
setID
title
description
version
}
chatSettings {
autoModLevel
blockLinks
chatDelayMs
followersOnlyDurationMinutes
isBroadcasterLanguageModeEnabled
isEmoteOnlyModeEnabled
isFastSubsModeEnabled
isOptedOutOfGlobalBannedWordsList
isSubscribersOnlyModeEnabled
isUniqueChatModeEnabled
requireVerifiedAccount
rules
slowModeDurationSeconds
}
stream {
averageFPS
bitrate
codec
createdAt
width
height
id
viewersCount
type
game{displayName}
}
lastBroadcast {
game{displayName}
id
startedAt
title
}
channel {
chatters {
count
moderators {
login
}
vips {
login
}
}
}
}
}
)
queries << query
end
queries.each do |query|
spawn do
result = JSON.parse(req(query))
channel.send(result)
end
end
queries.each do
responses << channel.receive
end
return responses
end
def getModsVips(params)
query = %( query = %(
query { query {
user(#{user(env)}) { user(login: "#{params}" lookupType: ALL) {
mods(first: 100) { mods(first: 100) {
edges { edges {
grantedAt grantedAt
@ -169,13 +279,13 @@ module GqlAPI
} }
} }
) )
return (JSON.parse(gqlReq(query)))["data"] return JSON.parse(req(query))
end end
def getFounders(env) def getFounders(params)
query = %( query = %(
query { query {
user(#{user(env)}) { user(login: "#{params}" lookupType: ALL) {
id id
login login
displayName displayName
@ -194,13 +304,13 @@ module GqlAPI
} }
} }
) )
return (JSON.parse(gqlReq(query)))["data"] return JSON.parse(req(query))
end end
def getClips(params) def getClips(params)
query = %( query = %(
query { query {
clip(slug: "#{params["slug"]}") { clip(slug: "#{params}") {
createdAt createdAt
creationState creationState
durationSeconds durationSeconds
@ -234,14 +344,14 @@ module GqlAPI
} }
} }
) )
return (JSON.parse(gqlReq(query)))["data"] return JSON.parse(req(query))
end end
def getSubage(user, channel) def getSubage(user, channel)
channelId = HelixAPI.getId(channel) channelId = HelixAPI.getId(channel)
query = %( query = %(
query { query {
userData: userResultByLogin (login: "#{user}") { userData: userResultByLogin (login: "#{user}") {
... on UserDoesNotExist { key reason } ... on UserDoesNotExist { key reason }
... on UserError { key } ... on UserError { key }
... on User { id displayName login } ... on User { id displayName login }
@ -287,7 +397,7 @@ module GqlAPI
} }
} }
) )
return (JSON.parse(gqlReq(query)))["data"] return JSON.parse(req(query))
end end
def getEmotes(emote) def getEmotes(emote)
@ -318,7 +428,7 @@ module GqlAPI
} }
} }
) )
return (JSON.parse(gqlReq(query)))["data"] return JSON.parse(req(query))
end end
def getChannelEmotes(channel) def getChannelEmotes(channel)
@ -352,7 +462,7 @@ module GqlAPI
} }
} }
) )
return (JSON.parse(gqlReq(query)))["data"] return JSON.parse(req(query))
end end
def getEmotes(emote) def getEmotes(emote)
@ -383,7 +493,7 @@ module GqlAPI
} }
} }
) )
return (JSON.parse(gqlReq(query)))["data"] return JSON.parse(req(query))
end end
def getChannelBadges(channel) def getChannelBadges(channel)
@ -410,12 +520,18 @@ module GqlAPI
} }
} }
) )
return (JSON.parse(gqlReq(query)))["data"] return JSON.parse(req(query))
end end
def gqlReq(query) def req(query)
headers = HTTP::Headers{
"Content-Type" => "application/json",
# "Client-Id" => "kimne78kx3ncx6brgo4mv6wki5h1ko", # Can cause problems due to Client-Integrity
"Authorization" => "OAuth #{CONFIG.gqlOAuth}",
"Client-Id" => "#{CONFIG.gqlClientID}",
}
data = {"query" => query} data = {"query" => query}
response = HTTP::Client.post(CONFIG.gqlEndpoint.to_s, headers: @@headers, body: data.to_json) response = HTTP::Client.post(CONFIG.gqlEndpoint.to_s, headers: headers, body: data.to_json)
if response.success? if response.success?
return (response.body) return (response.body)

View file

@ -1,40 +0,0 @@
module HelixAPI
extend self
@@headers = HTTP::Headers{
"Content-Type" => "application/json",
"Authorization" => "Bearer #{CONFIG.helixOAuth}",
"Client-Id" => "#{CONFIG.helixClientID}",
}
def 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 getId(login)
request = "#{CONFIG.apiEndpoint.to_s}/users?login=#{login}"
return JSON.parse(helixReq(request))["data"][0]["id"]
end
def helixReq(request)
response = HTTP::Client.get(request, headers: @@headers)
if response.success?
return (response.body)
else
raise "Helix Twitch API returned #{response.status_code}: #{response.body.to_s}"
end
end
end

23
src/twitchapi/helixapi.cr Normal file
View file

@ -0,0 +1,23 @@
module HelixAPI
extend self
def getId(login)
request = "#{CONFIG.apiEndpoint.to_s}/users?login=#{login}"
return JSON.parse(req(request))["data"][0]["id"]
end
def req(request)
headers = HTTP::Headers{
"Content-Type" => "application/json",
"Authorization" => "Bearer #{CONFIG.helixOAuth}",
"Client-Id" => "#{CONFIG.helixClientID}",
}
response = HTTP::Client.get(request, headers: headers)
if response.success?
return (response.body)
else
raise "Helix Twitch API returned #{response.status_code}: #{response.body.to_s}"
end
end
end

17
src/utils/redis.cr Normal file
View file

@ -0,0 +1,17 @@
module TwAPI::Utils::Redis
extend self
def getExpireTime(key)
return REDIS_DB.ttl(key)
end
def retrieveFromCache(keyName)
if keyName && (json = REDIS_DB.get(keyName))
return json
end
end
def saveJson(keyName, json_data : String)
REDIS_DB.set(keyName, json_data, ex: 60)
end
end

3
src/utils/utils.cr Normal file
View file

@ -0,0 +1,3 @@
module TwAPI::Utils
extend self
end