Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
408 changes: 332 additions & 76 deletions .idea/twitchTracker.iml

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,9 @@ gem "rest-client", "~> 2.1"
gem 'awesome_print', :require => 'ap'

gem "httparty", "~> 0.21.0"

gem "faraday", "~> 2.9"

gem "faraday-parse_dates", "~> 0.1.1"

gem "faraday-retry", "~> 2.2"
14 changes: 14 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ GEM
erubi (1.12.0)
et-orbi (1.2.7)
tzinfo
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday-net_http (3.1.0)
net-http
faraday-parse_dates (0.1.1)
faraday (~> 2.0)
faraday-retry (2.2.0)
faraday (~> 2.0)
figaro (1.2.0)
thor (>= 0.14.0, < 2)
fugit (1.9.0)
Expand Down Expand Up @@ -130,6 +138,8 @@ GEM
msgpack (1.7.2)
multi_xml (0.6.0)
mutex_m (0.2.0)
net-http (0.4.1)
uri
net-imap (0.4.10)
date
net-protocol
Expand Down Expand Up @@ -233,6 +243,7 @@ GEM
timeout (0.4.1)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uri (0.13.0)
webrick (1.8.1)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
Expand All @@ -251,6 +262,9 @@ DEPENDENCIES
awesome_print
bootsnap
debug
faraday (~> 2.9)
faraday-parse_dates (~> 0.1.1)
faraday-retry (~> 2.2)
figaro
httparty (~> 0.21.0)
pg (~> 1.1)
Expand Down
4 changes: 4 additions & 0 deletions app/services/twitch-api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

require_relative 'twitch/version'
require_relative 'twitch/client'
18 changes: 18 additions & 0 deletions app/services/twitch/api_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Twitch
# An error returned by the API.
class APIError < StandardError
# HTTP status code of the response.
attr_reader :status_code
# Body content of the response.
attr_reader :body

def initialize(status_code, body)
@status_code = status_code
@body = body

super(self.body['message'])
Copy link

Copilot AI May 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure that the response body includes a 'message' key or provide a default error message to avoid passing nil to StandardError.

Suggested change
super(self.body['message'])
super(self.body&.dig('message') || "An error occurred")

Copilot uses AI. Check for mistakes.
end
Comment on lines +11 to +16
Copy link

@coderabbitai coderabbitai bot May 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Guard against missing or non-hash body['message'].
Calling body['message'] directly can blow up if body isn’t a Hash or lacks message. Fallback to a safe representation:

     def initialize(status_code, body)
       @status_code = status_code
       @body = body

-      super(self.body['message'])
+      msg = body.is_a?(Hash) ? (body['message'] || body.to_s) : body.to_s
+      super(msg)
     end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def initialize(status_code, body)
@status_code = status_code
@body = body
super(self.body['message'])
end
def initialize(status_code, body)
@status_code = status_code
@body = body
msg = body.is_a?(Hash) ? (body['message'] || body.to_s) : body.to_s
super(msg)
end
🤖 Prompt for AI Agents
In app/services/twitch/api_error.rb around lines 11 to 16, the code directly
accesses body['message'] which can cause errors if body is not a Hash or does
not contain the 'message' key. To fix this, add a guard to safely check if body
is a Hash and has the 'message' key before accessing it; otherwise, provide a
fallback string or the raw body content when calling super to avoid exceptions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabiitai can explain me this in more stupid way

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

end
end
17 changes: 17 additions & 0 deletions app/services/twitch/automod_message_status.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Twitch
# A determination if AutoMod would allow a message in a particular channel's chat.
class AutomodMessageStatus
# ID of the message. Matches the field of the same name passed in the request.
attr_reader :msg_id
# Whether the message would meet AutoMod requirements for the channel.
attr_reader :is_permitted

def initialize(attributes = {})
attributes.each do |key, value|
instance_variable_set :"@#{key}", value
end
end
end
end
23 changes: 23 additions & 0 deletions app/services/twitch/bits_leader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module Twitch
# A user that is a leader for bits.
class BitsLeader
# ID of the user giving bits.
attr_reader :user_id
# Display name of the user giving bits.
attr_reader :user_name
# Ranking of the user giving bits.
# Reflects the parent object's date range.
attr_reader :rank
# Number of bits given in the parent object's date range.
attr_reader :score

def initialize(attributes = {})
@user_id = attributes['user_id']
@user_name = attributes['user_name']
@rank = attributes['rank']
@score = attributes['score']
end
end
end
27 changes: 27 additions & 0 deletions app/services/twitch/category.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Twitch
## Data class for categories
class Category
## A URL to an image of the game’s box art or streaming category.
attr_reader :box_art_url

## The name of the game or category.
attr_reader :name

## An ID that uniquely identifies the game or category.
attr_reader :id

## Not always available:

## The ID that IGDB uses to identify this game.
## If the IGDB ID is not available to Twitch, this field is set to an empty string.
attr_reader :igdb_id

def initialize(attributes = {})
attributes.each do |key, value|
instance_variable_set :"@#{key}", value
end
end
end
end
46 changes: 46 additions & 0 deletions app/services/twitch/channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

module Twitch
# Data object for Twitch channels
# https://dev.twitch.tv/docs/api/reference#get-channel-information
class Channel
# Twitch User ID of this channel owner.
attr_reader :broadcaster_id
# Broadcaster's user login name.
attr_reader :broadcaster_login
# Twitch user display name of this channel owner.
attr_reader :broadcaster_name
# Language of the channel.
# A language value is either the ISO 639-1 two-letter code for a supported stream language
# or "other".
attr_reader :broadcaster_language
# Current game ID being played on the channel .
attr_reader :game_id
# Name of the game being played on the channel.
attr_reader :game_name
# Title of the stream.
attr_reader :title
# Stream delay in seconds.
attr_reader :delay

# Some endpoints, like "Search Channels", can return different fields
attr_reader :id
attr_reader :display_name
# A Boolean value that determines whether the broadcaster is streaming live.
# Is true if the broadcaster is streaming live; otherwise, false.
attr_reader :is_live
# The tags applied to the channel.
attr_reader :tags
# A URL to a thumbnail of the broadcaster’s profile image.
attr_reader :thumbnail_url
# The UTC date and time (in RFC3339 format) of when the broadcaster started streaming.
# The string is empty if the broadcaster is not streaming live.
attr_reader :started_at

def initialize(attributes = {})
attributes.each do |key, value|
instance_variable_set :"@#{key}", value
end
end
end
end
53 changes: 53 additions & 0 deletions app/services/twitch/cheermote.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

require_relative 'cheermote_tier'

Comment on lines +1 to +4
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Require the time library to parse timestamps

last_updated should be a proper Time object. Make sure the file can parse it:

 # frozen_string_literal: true
+
+require 'time'
 require_relative 'cheermote_tier'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# frozen_string_literal: true
require_relative 'cheermote_tier'
# frozen_string_literal: true
require 'time'
require_relative 'cheermote_tier'
🤖 Prompt for AI Agents
In app/services/twitch/cheermote.rb at the top of the file (lines 1 to 4), add a
require statement for the 'time' library to enable parsing of timestamps. This
will ensure that the last_updated attribute can be properly converted into a
Time object when parsed.

module Twitch
## Data class for Cheermotes, animated emotes that viewers can assign Bits to.
class Cheermote
## The name portion of the Cheermote string that you use in chat to cheer Bits.
## The full Cheermote string is the concatenation of {prefix} + {number of Bits}.
## For example, if the prefix is “Cheer” and you want to cheer 100 Bits,
## the full Cheermote string is Cheer100.
## When the Cheermote string is entered in chat,
## Twitch converts it to the image associated with the Bits tier that was cheered.
attr_reader :prefix

## A list of tier levels that the Cheermote supports.
## Each tier identifies the range of Bits that you can cheer at that tier level
## and an image that graphically identifies the tier level.
attr_reader :tiers

## The type of Cheermote. Possible values are:
## * global_first_party — A Twitch-defined Cheermote that is shown in the Bits card.
## * global_third_party — A Twitch-defined Cheermote that is not shown in the Bits card.
## * channel_custom — A broadcaster-defined Cheermote.
## * display_only — Do not use; for internal use only.
## * sponsored — A sponsor-defined Cheermote.
## When used, the sponsor adds additional Bits to the amount that the user cheered.
## For example, if the user cheered Terminator100, the broadcaster might receive 110 Bits,
## which includes the sponsor's 10 Bits contribution.
attr_reader :type

## The order that the Cheermotes are shown in the Bits card.
## The numbers may not be consecutive. For example, the numbers may jump from 1 to 7 to 13.
## The order numbers are unique within a Cheermote type (for example, global_first_party)
## but may not be unique amongst all Cheermotes in the response.
attr_reader :order

## The date and time when this Cheermote was last updated.
attr_reader :last_updated

## A Boolean value that indicates whether this Cheermote provides
## a charitable contribution match during charity campaigns.
attr_reader :is_charitable

def initialize(attributes = {})
attributes.each do |key, value|
value = value.map { |tier| CheermoteTier.new(tier) } if key == 'tiers'

instance_variable_set :"@#{key}", value
end
end
Comment on lines +45 to +51
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Handle symbol keys & convert last_updated and tiers safely

attributes may arrive with symbol keys (:tiers, :last_updated).
Current string comparison misses those, leaving raw data in the object.

def initialize(attributes = {})
-  attributes.each do |key, value|
-    value = value.map { |tier| CheermoteTier.new(tier) } if key == 'tiers'
-
-    instance_variable_set :"@#{key}", value
-  end
+  attributes.each do |key, value|
+    case key.to_sym
+    when :tiers
+      @tiers = value.map { |tier| CheermoteTier.new(tier) }
+    when :last_updated
+      @last_updated = Time.parse(value)
+    else
+      instance_variable_set :"@#{key}", value
+    end
+  end
 end

This guarantees type-safe fields and works regardless of key type.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def initialize(attributes = {})
attributes.each do |key, value|
value = value.map { |tier| CheermoteTier.new(tier) } if key == 'tiers'
instance_variable_set :"@#{key}", value
end
end
def initialize(attributes = {})
attributes.each do |key, value|
case key.to_sym
when :tiers
@tiers = value.map { |tier| CheermoteTier.new(tier) }
when :last_updated
@last_updated = Time.parse(value)
else
instance_variable_set :"@#{key}", value
end
end
end
🤖 Prompt for AI Agents
In app/services/twitch/cheermote.rb around lines 45 to 51, the initialize method
only handles string keys like 'tiers' and does not process symbol keys such as
:tiers or :last_updated, which can cause raw data to be assigned instead of
properly typed objects. Update the method to handle both string and symbol keys
by normalizing keys or checking for both forms, and ensure that the 'tiers' key
always converts its values to CheermoteTier instances and 'last_updated' is
safely converted to the appropriate type regardless of key format.

end
end
55 changes: 55 additions & 0 deletions app/services/twitch/cheermote_tier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

require_relative 'cheermote_tier_images'

module Twitch
## Data class for Cheermote Tier.
## Each tier identifies the range of Bits that you can cheer at that tier level
## and an image that graphically identifies the tier level.
class CheermoteTier
## The minimum number of Bits that you must cheer at this tier level.
## The maximum number of Bits that you can cheer at this level
## is determined by the required minimum Bits of the next tier level minus 1.
## For example, if `min_bits` is 1 and `min_bits` for the next tier is 100,
## the Bits range for this tier level is 1 through 99.
## The minimum Bits value of the last tier is the maximum number of Bits you can cheer
## using this Cheermote. For example, 10000.
attr_reader :min_bits

## The tier level. Possible tiers are:
## * 1
## * 100
## * 500
## * 1000
## * 5000
## * 10000
## * 100000
attr_reader :id

## The hex code of the color associated with this tier level (for example, `#979797`).
attr_reader :color

## The animated and static image sets for the Cheermote.
## The dictionary of images is organized by theme, format, and size.
## The theme keys are `dark` and `light`.
## Each theme is a dictionary of formats: `animated` and `static`.
## Each format is a dictionary of sizes: `1`, `1.5`, `2`, `3`, and `4`.
## The value of each size contains the URL to the image.
attr_reader :images

## A Boolean value that determines whether users can cheer at this tier level.
attr_reader :can_cheer

## A Boolean value that determines whether this tier level is shown in the Bits card.
## Is `true` if this tier level is shown in the Bits card.
attr_reader :show_in_bits_card

def initialize(attributes = {})
attributes.each do |key, value|
value = CheermoteTierImages.new(value) if key == 'images'

instance_variable_set :"@#{key}", value
end
end
Comment on lines +47 to +53
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Symbol-key support & strong typing for images

Like Cheermote, the key comparison should not rely on string literals only.
Additionally, validating that the incoming hash actually looks like an images
structure helps catch API regressions early.

attributes.each do |key, value|
-  value = CheermoteTierImages.new(value) if key == 'images'
+  if key.to_sym == :images
+    value = CheermoteTierImages.new(value)
+  end
   instance_variable_set :"@#{key}", value
end

Consider raising if value is not a Hash to avoid silent corruption.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def initialize(attributes = {})
attributes.each do |key, value|
value = CheermoteTierImages.new(value) if key == 'images'
instance_variable_set :"@#{key}", value
end
end
def initialize(attributes = {})
attributes.each do |key, value|
- value = CheermoteTierImages.new(value) if key == 'images'
+ if key.to_sym == :images
+ value = CheermoteTierImages.new(value)
+ end
instance_variable_set :"@#{key}", value
end
end
🤖 Prompt for AI Agents
In app/services/twitch/cheermote_tier.rb around lines 47 to 53, the initialize
method currently checks for the 'images' key using a string literal and does not
validate the value type. Update the key comparison to support both string and
symbol keys for 'images'. Add a type check to ensure the value is a Hash before
initializing CheermoteTierImages, and raise an error if it is not, to prevent
silent data corruption and catch API regressions early.

end
end
18 changes: 18 additions & 0 deletions app/services/twitch/cheermote_tier_image.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Twitch
## Data class for Cheermote Tier image, static and animated
class CheermoteTierImage
## Each theme is a dictionary of formats: `animated` and `static`.
## Each format is a dictionary of sizes: `1`, `1.5`, `2`, `3`, and `4`.
## The value of each size contains the URL to the image.
attr_reader :animated
attr_reader :static

def initialize(attributes = {})
attributes.each do |key, value|
instance_variable_set :"@#{key}", value
end
end
end
end
21 changes: 21 additions & 0 deletions app/services/twitch/cheermote_tier_images.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require_relative 'cheermote_tier_image'

module Twitch
## Data class for Cheermote Tier images by themes
class CheermoteTierImages
## The theme keys are `dark` and `light`.
## Each theme is a dictionary of formats: `animated` and `static`.
## Each format is a dictionary of sizes: `1`, `1.5`, `2`, `3`, and `4`.
## The value of each size contains the URL to the image.
attr_reader :dark
attr_reader :light

def initialize(attributes = {})
attributes.each do |key, value|
instance_variable_set :"@#{key}", CheermoteTierImage.new(value)
end
end
end
end
Loading