itch_rewards 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 199cd42ce4dbbe3f220a6bcc486278eef1702e6e696ec690c30ee25d5bea73a7
4
+ data.tar.gz: 3d4830e0ec9ff9f8011478077af6fa11a27b9e153583a09f76f4f3c72cb74011
5
+ SHA512:
6
+ metadata.gz: 49644f0519d13cdd27f74185a735664404933c2d93ec3b51ca25ec2d9529526209e655db47743f34e83ed927a1bcf21ed453661d57eedf9e4fcb23226c083a33
7
+ data.tar.gz: c431a320c9c5ec599f0bdab00eba1cbef4e57c2ce6f38e0edb3a0a48d39e62940ff50705f4c3d41b48318f0ced2f596048ddbe483087994cfd966dd7f0152b42
@@ -0,0 +1,16 @@
1
+ name: Ruby
2
+
3
+ on: [push,pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v2
10
+ - name: Set up Ruby
11
+ uses: ruby/setup-ruby@v1
12
+ with:
13
+ ruby-version: 3.0.1
14
+ bundler-cache: true
15
+ - name: Run the default task
16
+ run: bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+ .itch-cookies.yml
13
+ itch-reward-config.yml
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2021-05-23
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "rake", "~> 13.0"
8
+
9
+ gem "rspec", "~> 3.0"
10
+
11
+ gem "irb", "~> 1.3"
data/Gemfile.lock ADDED
@@ -0,0 +1,106 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ itch_rewards (0.1.0)
5
+ dry-cli (~> 0.7.0)
6
+ itch_client (~> 0.2.0)
7
+ pastel (~> 0.8.0)
8
+ tty-prompt (~> 0.23.1)
9
+ tty-table (~> 0.12.0)
10
+
11
+ GEM
12
+ remote: https://rubygems.org/
13
+ specs:
14
+ addressable (2.7.0)
15
+ public_suffix (>= 2.0.2, < 5.0)
16
+ connection_pool (2.2.5)
17
+ diff-lcs (1.4.4)
18
+ domain_name (0.5.20190701)
19
+ unf (>= 0.0.5, < 1.0.0)
20
+ dry-cli (0.7.0)
21
+ http-cookie (1.0.3)
22
+ domain_name (~> 0.5)
23
+ io-console (0.5.9)
24
+ irb (1.3.5)
25
+ reline (>= 0.1.5)
26
+ itch_client (0.2.0)
27
+ mechanize (~> 2.8)
28
+ mechanize (2.8.1)
29
+ addressable (~> 2.7)
30
+ domain_name (~> 0.5, >= 0.5.20190701)
31
+ http-cookie (~> 1.0, >= 1.0.3)
32
+ mime-types (~> 3.0)
33
+ net-http-digest_auth (~> 1.4, >= 1.4.1)
34
+ net-http-persistent (>= 2.5.2, < 5.0.dev)
35
+ nokogiri (~> 1.11, >= 1.11.2)
36
+ rubyntlm (~> 0.6, >= 0.6.3)
37
+ webrick (~> 1.7)
38
+ webrobots (~> 0.1.2)
39
+ mime-types (3.3.1)
40
+ mime-types-data (~> 3.2015)
41
+ mime-types-data (3.2021.0225)
42
+ net-http-digest_auth (1.4.1)
43
+ net-http-persistent (4.0.1)
44
+ connection_pool (~> 2.2)
45
+ nokogiri (1.11.6-x86_64-linux)
46
+ racc (~> 1.4)
47
+ pastel (0.8.0)
48
+ tty-color (~> 0.5)
49
+ public_suffix (4.0.6)
50
+ racc (1.5.2)
51
+ rake (13.0.3)
52
+ reline (0.2.5)
53
+ io-console (~> 0.5)
54
+ rspec (3.10.0)
55
+ rspec-core (~> 3.10.0)
56
+ rspec-expectations (~> 3.10.0)
57
+ rspec-mocks (~> 3.10.0)
58
+ rspec-core (3.10.1)
59
+ rspec-support (~> 3.10.0)
60
+ rspec-expectations (3.10.1)
61
+ diff-lcs (>= 1.2.0, < 2.0)
62
+ rspec-support (~> 3.10.0)
63
+ rspec-mocks (3.10.2)
64
+ diff-lcs (>= 1.2.0, < 2.0)
65
+ rspec-support (~> 3.10.0)
66
+ rspec-support (3.10.2)
67
+ rubyntlm (0.6.3)
68
+ strings (0.2.1)
69
+ strings-ansi (~> 0.2)
70
+ unicode-display_width (>= 1.5, < 3.0)
71
+ unicode_utils (~> 1.4)
72
+ strings-ansi (0.2.0)
73
+ tty-color (0.6.0)
74
+ tty-cursor (0.7.1)
75
+ tty-prompt (0.23.1)
76
+ pastel (~> 0.8)
77
+ tty-reader (~> 0.8)
78
+ tty-reader (0.9.0)
79
+ tty-cursor (~> 0.7)
80
+ tty-screen (~> 0.8)
81
+ wisper (~> 2.0)
82
+ tty-screen (0.8.1)
83
+ tty-table (0.12.0)
84
+ pastel (~> 0.8)
85
+ strings (~> 0.2.0)
86
+ tty-screen (~> 0.8)
87
+ unf (0.1.4)
88
+ unf_ext
89
+ unf_ext (0.0.7.7)
90
+ unicode-display_width (2.0.0)
91
+ unicode_utils (1.4.0)
92
+ webrick (1.7.0)
93
+ webrobots (0.1.2)
94
+ wisper (2.0.1)
95
+
96
+ PLATFORMS
97
+ x86_64-linux
98
+
99
+ DEPENDENCIES
100
+ irb (~> 1.3)
101
+ itch_rewards!
102
+ rake (~> 13.0)
103
+ rspec (~> 3.0)
104
+
105
+ BUNDLED WITH
106
+ 2.2.17
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Billiam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # itch-rewards
2
+
3
+ Commandline tool to update game rewards on Itch.io, and automatically update reward counts and descriptions based on sales.
4
+
5
+ ## Installation
6
+
7
+ $ gem install itch_rewards
8
+
9
+ ## Usage
10
+
11
+ ```shell
12
+ Usage: itch-rewards COMMAND [options]
13
+
14
+ Commands:
15
+ itch-rewards list # List all rewards for a game
16
+ itch-rewards list-games # List all games
17
+ itch-rewards recalculate # Update reward quantity and description from configuration file
18
+ itch-rewards setup # Save cookies for itch.io and create reward config example file
19
+ itch-rewards update GAME_ID REWARD_ID # Update a reward
20
+ itch-rewards version # Print version
21
+ ```
22
+
23
+ ### Authenticating
24
+
25
+ First, enable two-factor authentication for your itch.io account, if you haven't already. This prevents captcha prompts during login, which `itch-rewards` doesn't handle.
26
+
27
+ Then, run:
28
+
29
+ ```shell
30
+ $ itch-rewards setup
31
+ ```
32
+
33
+ You'll be prompted for your username, password, two-factor code, and a path to save a cookie file. Your credentials will not be saved, but the cookie will be used for subsequent logins (until the cookie expires).
34
+
35
+ You can use `itch-rewards` for multiple accounts by specifying a different cookie file path for each account:
36
+
37
+ ```shell
38
+ $ itch-rewards setup --cookie-path my_first_account.yml
39
+ # ...
40
+ $ itch-rewards setup --cookie-path my_second_account.yml
41
+ # ...
42
+ $ itch-rewards list-games --cookie-path my_first_account.yml
43
+ ```
44
+
45
+ You'll also be prompted to create an (optional) configuration file that can be used when automatically updating itch reward quantities and descriptions.
46
+
47
+ While logging in via cookies is easier (and required for non-interactive login, ex: for cron tasks), all commands also accept the following options.
48
+
49
+ ```shell
50
+ --username=VALUE, -u VALUE # Itch username
51
+ --password=VALUE, -p VALUE # Itch password
52
+ --cookie-path=VALUE # Path to cookies file for future logins, default: ".itch-cookies.yml"
53
+ --[no-]cookies # Enable cookie storage, default: true
54
+ --[no-]interactive # Enable interactive prompts, default: true
55
+ --help, -h # Print help
56
+ ```
57
+
58
+ ### List games
59
+
60
+ Return a list of game names and IDs. Useful for other commands that use game ID, or when creating a [reward configuration file](#configuration_file).
61
+
62
+
63
+ ```shell
64
+ Usage:
65
+ itch-rewards list-games
66
+
67
+ Description:
68
+ List all games
69
+ ```
70
+
71
+
72
+ ### List rewards
73
+
74
+ Show reward information for a single game.
75
+
76
+ Accepts either a game name, or game ID.
77
+ ```shell
78
+ Usage:
79
+ itch-rewards list
80
+
81
+ Description:
82
+ List all rewards for a game
83
+
84
+ Options:
85
+ --id=VALUE # Game ID
86
+ --name=VALUE # Game name
87
+
88
+ Examples:
89
+ itch-rewards list --id 123456 # List rewards for game with ID 123456
90
+ itch-rewards list --name MyGame # List rewards for game with name MyGame
91
+ ```
92
+
93
+ ### Update a reward
94
+
95
+ Update a single reward for a game. Quantity, title, description, price and archive status can be changed.
96
+
97
+ ```shell
98
+ Command:
99
+ itch-rewards update
100
+
101
+ Usage:
102
+ itch-rewards update GAME_ID REWARD_ID
103
+
104
+ Description:
105
+ Update a reward
106
+
107
+ Arguments:
108
+ GAME_ID # REQUIRED Game with the reward to edit
109
+ REWARD_ID # REQUIRED Reward ID to update
110
+
111
+ Options:
112
+ --quantity=VALUE # Reward quantity (total, including redeemed)
113
+ --title=VALUE # Reward title
114
+ --[no-]archived # Reward archived status
115
+ --description=VALUE # Reward description
116
+ --price=VALUE # Reward price without currency (ex: 15.99)
117
+
118
+ Examples:
119
+ itch-rewards update 123456 78910 --quantity 5 # Set the reward count to 5 for reward ID 78910 in game ID 123456
120
+ itch-rewards update 123456 78910 --price 5.00 --archived # Set reward price to 5.00 and archive it
121
+ ```
122
+
123
+ ### Automated reward updates
124
+
125
+ If you wish to update a reward description, or available quantity based on purchases or tips.
126
+
127
+ ```shell
128
+ Usage:
129
+ itch-rewards recalculate
130
+
131
+ Description:
132
+ Update reward quantity and description from configuration file
133
+
134
+ Options:
135
+ --config=VALUE # Path to config file, default: "itch-reward-config.yml"
136
+ --[no-]save # Saves changes when enabled. Otherwise, dry-run and show result, default: false
137
+ ```
138
+
139
+ #### Example reward scenarios
140
+
141
+ > I want every purchase to add one community copy
142
+
143
+ ```yml
144
+ MyGame:
145
+ id: 123456
146
+ reward_id: 789012
147
+ reward_by_purchase: 1
148
+ reward_by_tip: 0.0
149
+ reward_offset: 0
150
+ minimum_available: 0
151
+ ```
152
+
153
+
154
+ > I want every two purchases to add one community copy
155
+ ```yml
156
+ MyGame:
157
+ id: 123456
158
+ reward_id: 789012
159
+ reward_by_purchase: 0.5
160
+ reward_by_tip: 0.0
161
+ reward_offset: 0
162
+ minimum_available: 0
163
+ ```
164
+
165
+ > I want tips over the purchase price to add proportional community copies
166
+
167
+
168
+ ```yml
169
+ MyGame:
170
+ id: 123456
171
+ reward_id: 789012
172
+ reward_by_purchase: 0
173
+ reward_by_tip: 0.0
174
+ reward_offset: 0
175
+ minimum_available: 0
176
+ ```
177
+
178
+ For example: a $5 tip, on a $10 game will contribute 0.5 copies to the reward pool when `reward_by_tip` is `1`.
179
+
180
+ The formula for this is: `(tip_amount / game_price) * reward_by_tip`
181
+
182
+
183
+ > I want five community copies to always be available
184
+
185
+ ```yml
186
+ MyGame:
187
+ id: 123456
188
+ reward_id: 789012
189
+ reward_by_purchase: 0
190
+ reward_by_tip: 0.0
191
+ reward_offset: 0
192
+ minimum_available: 5
193
+ ```
194
+
195
+ #### Updating reward description
196
+
197
+ If present, the `reward_description_template` configuration value can be used to change the description of your reward with information about the reward itself.
198
+
199
+ For instance:
200
+
201
+ ```yml
202
+ reward_description_template: <p>Rewards added: { amount }</p>
203
+ ```
204
+
205
+ The above will change the reward description to "Rewards added: 10".
206
+ The following placehoder values are available:
207
+ * `{ amount }`: The total number of reward copies in the pool, including redeemed rewards.
208
+ * `{ remaining_percent }`: A number between 0.0 and 100.0, indicating the percentage until the next reward
209
+ * `{ remaining_percent_integer }`: A number between 0 and 100. As above (but with no decimal value included).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: %i[spec]
data/bin/console ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "itch_rewards"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+
16
+ def reload!
17
+ root_dir = File.expand_path("..", __dir__)
18
+ reload_dirs = %w[lib]
19
+ reload_dirs.each do |dir|
20
+ Dir.glob("#{root_dir}/#{dir}/**/*.rb").each { |f| load(f) }
21
+ end
22
+ puts "Reloaded"
23
+ end
24
+
25
+ IRB.start(__FILE__)
data/bin/itch-rewards ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require "bundler/setup"
6
+ require "itch_rewards/cli"
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/itch_rewards/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "itch_rewards"
7
+ spec.version = ItchRewards::VERSION
8
+ spec.authors = ["Billiam"]
9
+ spec.email = ["billiamthesecond@gmail.com"]
10
+
11
+ spec.summary = "Itch community copy automation utility"
12
+ spec.description = "Automatically update available rewards based on purchases"
13
+ spec.homepage = "https://github.com/Billiam/itch-community-rewards"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "bin"
27
+ spec.executables = ['itch-rewards']
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency "itch_client", "~> 0.2.0"
31
+ spec.add_dependency "dry-cli", "~> 0.7.0"
32
+ spec.add_dependency "tty-prompt", "~> 0.23.1"
33
+ spec.add_dependency "tty-table", "~> 0.12.0"
34
+ spec.add_dependency "pastel", "~> 0.8.0"
35
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "itch_rewards/version"
4
+
5
+ module ItchRewards
6
+ class Error < StandardError; end
7
+ end
@@ -0,0 +1,359 @@
1
+ require "dry/cli"
2
+ require "tty-prompt"
3
+ require "tty-table"
4
+ require "itch_client"
5
+ require "bigdecimal"
6
+ require "pastel"
7
+
8
+ module ItchRewards
9
+ class CLI
10
+ module AuthOptions
11
+ def self.included(base)
12
+ base.class_eval do
13
+ option :username, desc: "Itch username", aliases: ["u"]
14
+ option :password, desc: "Itch password", aliases: ["p"]
15
+ option :cookie_path, desc: "Path to cookies file for future logins", default: ".itch-cookies.yml"
16
+ option :cookies, desc: "Enable cookie storage", type: :boolean, default: true
17
+ option :interactive, type: :boolean, desc: "Enable interactive prompts", default: true
18
+ end
19
+ end
20
+ end
21
+
22
+ module Helper
23
+ def cli
24
+ @cli ||= begin
25
+ TTY::Prompt.new
26
+ end
27
+ end
28
+
29
+ def color
30
+ @pastel ||= Pastel.new
31
+ end
32
+
33
+ def authenticated_client(options)
34
+ @authenticated_client ||= begin
35
+ interactive = options[:interactive]
36
+
37
+ username = options[:username] || -> { interactive ? cli.ask("Itch.io username:", required: true) : (cli.error("Username required but not provided"); exit 1) }
38
+ password = options[:password] || -> { interactive ? cli.mask("Itch.io password:", required: true) : (cli.error("Password required but not provided"); exit 1) }
39
+ totp = -> { interactive ? cli.mask("Enter your 2FA code", required: true) : (cli.error("Cannot enter totp code in non-interactive mode"); exit 1) }
40
+
41
+ cookie_path = options[:cookies] ? options[:cookie_path] : nil
42
+
43
+ client = Itch.new(username: username, password: password, cookie_path: cookie_path)
44
+ client.totp = totp
45
+
46
+ client.login && client
47
+ rescue Itch::AuthError => e
48
+ cli.error(e.message)
49
+ nil
50
+ end
51
+ end
52
+
53
+ def authenticated_client!(options)
54
+ authenticated_client(options) || exit(1)
55
+ end
56
+
57
+ def objects_to_table(objects)
58
+ objects = Array(objects)
59
+ return nil if objects.none?
60
+ if objects.first.is_a? Hash
61
+ headers = objects.first.keys.sort
62
+ data = objects.map {|v| v.values_at(*headers) }
63
+ headers = headers.map(&:upcase)
64
+ else
65
+ fields = objects.first.instance_variables.sort
66
+ data = objects.map do |object|
67
+ fields.map {|k| object.instance_variable_get(k) }
68
+ end
69
+ headers = fields.map {|f| f.to_s[1..].upcase }
70
+ end
71
+
72
+ TTY::Table.new(headers, data)
73
+ end
74
+
75
+ def render_table(table)
76
+ return "No data" unless table
77
+
78
+ table.render(:unicode, multiline: true, padding: [0,1], resize: false, border: { style: :green })
79
+ end
80
+ end
81
+
82
+ module Commands
83
+ extend Dry::CLI::Registry
84
+
85
+ class Version < Dry::CLI::Command
86
+ include Helper
87
+
88
+ desc "Print version"
89
+
90
+ def call(*)
91
+ cli.say "ItchRewards #{ItchRewards::VERSION} (ItchClient #{Itch::VERSION})"
92
+ end
93
+ end
94
+
95
+ class Setup < Dry::CLI::Command
96
+ include Helper
97
+ include AuthOptions
98
+ @options = @options.reject {|opt| [:cookies, :interactive].include? opt.name }
99
+
100
+ desc "Save cookies for itch.io and create reward config example file"
101
+ def call(**options)
102
+ options[:cookies] ||= cli.ask("Where would you like to store your login cookies? ", default: ".itch-cookies.yml")
103
+ if authenticated_client(options)
104
+ cli.say "Saved cookies to #{options[:cookie_path]}"
105
+ else
106
+ cli.say "Login failed, cookies not saved"
107
+ end
108
+
109
+ config_path = "itch-reward-config.yml"
110
+
111
+ if !File.exist? config_path
112
+ result = cli.yes?("Config file #{config_path} does not exist, would you like to create it?")
113
+
114
+ if result
115
+ write_config(config_path)
116
+ cli.say "Config file written to #{config_path}"
117
+ end
118
+ else
119
+ cli.warn "Config file #{config_path} already exists, skipping..."
120
+ end
121
+ end
122
+ end
123
+
124
+ module Games
125
+ class List < Dry::CLI::Command
126
+ include AuthOptions
127
+ include Helper
128
+
129
+ desc "List all games"
130
+ def call(**options)
131
+ client = authenticated_client!(options)
132
+ table = objects_to_table(client.game_map.map.values)
133
+
134
+ cli.say "Games"
135
+ cli.say render_table(table)
136
+ end
137
+ end
138
+ end
139
+
140
+ module Rewards
141
+ extend self
142
+ include Helper
143
+ def self.show_rewards(game)
144
+ cli.say "Rewards for #{game.name} (id: #{game.id})"
145
+ table = objects_to_table(game.rewards.list)
146
+ cli.say render_table(table)
147
+ end
148
+
149
+ def self.write_config(path)
150
+ require 'erb'
151
+ client = authenticated_client!
152
+
153
+ games = client.game_map.map.values
154
+ template = File.read(File.join(__dir__, 'templates/reward_config.yml.erb'))
155
+
156
+ File.write(options[:config], ERB.new(template, trim_mode: '-').result(binding))
157
+ end
158
+
159
+ def self.load_config(path)
160
+ YAML.load_file(path)
161
+ rescue YAML::ParseError => e
162
+ cli.error("Config file (#{path}) is not valid yaml")
163
+ exit 1
164
+ end
165
+
166
+ class List < Dry::CLI::Command
167
+ include AuthOptions
168
+ include Helper
169
+
170
+ desc "List all rewards for a game"
171
+
172
+ option :id, type: :string, desc: "Game ID"
173
+ option :name, type: :string, desc: "Game name"
174
+
175
+ example [
176
+ "--id 123456 # List rewards for game with ID 123456",
177
+ "--name MyGame # List rewards for game with name MyGame"
178
+ ]
179
+ def call(**options)
180
+ if options[:id].nil? && options[:name].nil?
181
+ cli.error "Game ID or game name argument is required"
182
+ exit 1
183
+ end
184
+
185
+ client = authenticated_client!(options)
186
+ game = options[:id] ? client.game(options[:id]) : client.game(name: options[:name])
187
+
188
+ Rewards.show_rewards(game)
189
+ end
190
+ end
191
+
192
+ class Update < Dry::CLI::Command
193
+ include AuthOptions
194
+ include Helper
195
+
196
+ desc "Update a reward"
197
+
198
+ example [
199
+ "123456 78910 --quantity 5 # Set the reward count to 5 for reward ID 78910 in game ID 123456",
200
+ "123456 78910 --price 5.00 --archived # Set reward price to 5.00 and archive it"
201
+ ]
202
+
203
+ argument :game_id, required: true, desc: "Game with the reward to edit"
204
+ argument :reward_id, type: :integer, required: true, desc: "Reward ID to update"
205
+
206
+ option :quantity, desc: "Reward quantity (total, including redeemed)"
207
+ option :title, type: :string, desc: "Reward title"
208
+ option :archived, type: :boolean, desc: "Reward archived status"
209
+ option :description, type: :string, desc: "Reward description"
210
+ option :price, type: :string, desc: "Reward price without currency (ex: 15.99)"
211
+
212
+ def call(game_id:, reward_id:, **options)
213
+ client = authenticated_client!(options)
214
+ game = client.game(game_id)
215
+ rewards = game.rewards
216
+
217
+ reward_id = reward_id.to_i
218
+ reward_list = rewards.list
219
+
220
+ reward = reward_list.find {|reward| reward.id == reward_id }
221
+
222
+ unless reward
223
+ cli.error "Could not find reward with id: #{reward_id} for game #{game.name} (#{game.id})"
224
+ exit 1
225
+ end
226
+
227
+ unless options[:archived].nil?
228
+ reward.archived = options[:archived]
229
+ end
230
+
231
+ %i(amount description price title).each do |field|
232
+ if options[field]
233
+ reward.public_send("#{field}=", options[field])
234
+ end
235
+ end
236
+
237
+ rewards.save reward_list
238
+
239
+ Rewards.show_rewards(game)
240
+ end
241
+ end
242
+
243
+ class Automate < Dry::CLI::Command
244
+ include Helper
245
+ include AuthOptions
246
+
247
+ desc "Update reward quantity and description from configuration file"
248
+
249
+ option :config, required: true, desc: "Path to config file", default: "itch-reward-config.yml"
250
+ option :save, type: :boolean, desc: "Saves changes when enabled. Otherwise, dry-run and show result", default: false
251
+
252
+ def call(**options)
253
+ client = authenticated_client!(options)
254
+
255
+ if !File.exist? options[:config]
256
+ cli.error("Config file #{options[:config]} does not exist")
257
+ exit 1
258
+ end
259
+
260
+ config = Rewards.load_config(options[:config])
261
+ unless config["games"].is_a? Hash
262
+ cli.error("No games configured for rewards updates in config file")
263
+ exit 1
264
+ end
265
+
266
+ unless options[:save]
267
+ cli.warn "Dry run, results will not not saved"
268
+ end
269
+
270
+ purchases_by_game = client.purchases.history.each.group_by {|row| row['object_name'] }.to_h
271
+
272
+ config["games"].each do |name, data|
273
+ name = name.chomp
274
+
275
+ next unless data["reward_by_tip"] > 0 || data["reward_by_purchase"] > 0 || data["minimum_available"] > 0
276
+ game = client.game(data["id"])
277
+ rewards = game.rewards.list
278
+
279
+ reward = rewards.find {|r| r.id == data["reward_id"]}
280
+ unless reward
281
+ cli.warn "Could not find reward #{data["reward_id"]} for game #{name}, skipping..."
282
+ next
283
+ end
284
+
285
+ tip_modifier = data["reward_by_tip"].to_f
286
+ purchase_modifier = data["reward_by_purchase"].to_f
287
+ minimum = data["minimum_available"].to_i
288
+ template = data["reward_description_template"]
289
+
290
+ new_description = reward.description
291
+ new_amount = Array(purchases_by_game[name]).inject(data["reward_offset"]) do |sum, purchase|
292
+ price = purchase["product_price"].to_i
293
+ tip = (purchase["tip"].to_f * 100).to_i
294
+
295
+ next sum unless price > 0
296
+
297
+ sum += (tip.fdiv(price)) * tip_modifier
298
+ sum += purchase_modifier
299
+
300
+ sum
301
+ end
302
+ new_amount = [new_amount, reward.claimed + minimum].max if minimum > 0
303
+
304
+ if template && !template.empty?
305
+ new_description = template.gsub(/{ *(quantity|remaining_percent|remaining_percent_integer) *}/, "{\\1}")
306
+ .gsub(/{ *(quantity|remaining_percent|remaining_percent_integer) *}/,
307
+ "{quantity}" => new_amount.floor,
308
+ "{remaining_percent}" => ((new_amount % 1) * 100).floor(1),
309
+ "{remaining_percent_integer}" => ((new_amount % 1) * 100).floor
310
+ )
311
+ end
312
+
313
+ if new_amount.to_i != reward.amount
314
+ cli.say "Changing #{name} reward #{reward.id} quantity from #{color.green.bold(reward.amount.to_s)} to #{color.yellow.bold(new_amount.to_i)}"
315
+ end
316
+
317
+ if new_description != reward.description
318
+ cli.say "Changing #{name} reward #{reward.id} description to:\n#{color.yellow.bold(new_description)}"
319
+ end
320
+
321
+ if options[:save]
322
+ reward.description = new_description
323
+ reward.amount = new_amount.to_i
324
+
325
+ game.rewards.save rewards
326
+ end
327
+ end
328
+ end
329
+ end
330
+ end
331
+
332
+ register "version", Version, aliases: ["v", "-v", "--version"]
333
+ register "setup", Setup
334
+ register "list", Rewards::List
335
+ register "list-games", Games::List
336
+ register "update", Rewards::Update
337
+ register "recalculate", Rewards::Automate
338
+ end
339
+ end
340
+
341
+ class App < Dry::CLI
342
+ def usage_prefix
343
+ err.puts "Usage: #{ProgramName.call()} COMMAND [options]\n\n"
344
+ end
345
+
346
+ def usage_suffix
347
+ err.puts "\nGlobal options:\n -h # Show help for command"
348
+ end
349
+
350
+ def usage(result)
351
+ usage_prefix
352
+ err.puts Usage.call(result)
353
+ usage_suffix
354
+ exit(1)
355
+ end
356
+ end
357
+ end
358
+
359
+ ItchRewards::App.new(ItchRewards::CLI::Commands).call
@@ -0,0 +1,46 @@
1
+ ---
2
+ games:
3
+ <% if games.any? -%>
4
+ <% game = games.first -%>
5
+ # Game name, must match exactly
6
+ <%= game[:name] %>:
7
+
8
+ # Game ID
9
+ id: <%= game[:id] %>
10
+
11
+ # ID of reward to update
12
+ reward_id:
13
+
14
+ # Increase reward quantity by this value for each paid purchase
15
+ # ex: with reward_by_purchase set to 2, every purchase will add 2 reward copies
16
+ reward_by_purchase: 0
17
+
18
+ # Increase reward quantity by tip value, as a percentage of game price
19
+ # ex: with rewards_by_tip set to 1, a $10 tip on a $5 game (total price: $15) would add 2 reward copies
20
+ # ex: with rewards_by_tip set to 0.5, a $10 tip on a $5 game would add 1 reward copy
21
+ reward_by_tip: 0.0
22
+
23
+ # Add a flat value to reward quantity available (ex: for adding initial reward copies )
24
+ reward_offset: 0
25
+
26
+ # Minimum reward quantity available, regardless of other settings
27
+ # Ensures that there are at least this many rewards available
28
+ minimum_available: 0
29
+
30
+ # Can be used to update the reward description with reward information, like:
31
+ # total number of reward copies ({quantity}),
32
+ # or percentage until next copy ({remaining_percent} for a number between 0.0 and 100.0)
33
+ # or ({remaining_percent_integer} for a whole number between 0 and 100, rounded down)
34
+ # Ex: Please enjoy this community copy! <b>{ quantity }</b> copies added so far, { remaining_percent }% of the way to next reward!
35
+ reward_description_template:
36
+ <% games[1..].each do |game| %>
37
+ <%= game[:name] %>:
38
+ id: <%= game[:id] %>
39
+ reward_id:
40
+ reward_by_purchase: 0
41
+ reward_by_tip: 0.0
42
+ reward_offset: 0
43
+ minimum_available: 0
44
+ reward_description_template:
45
+ <% end %>
46
+ <% end %>
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ItchRewards
4
+ VERSION = "0.1.0"
5
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: itch_rewards
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Billiam
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-05-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: itch_client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-cli
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.7.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.7.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: tty-prompt
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.23.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.23.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: tty-table
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.12.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.12.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: pastel
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.8.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.8.0
83
+ description: Automatically update available rewards based on purchases
84
+ email:
85
+ - billiamthesecond@gmail.com
86
+ executables:
87
+ - itch-rewards
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".github/workflows/main.yml"
92
+ - ".gitignore"
93
+ - ".rspec"
94
+ - CHANGELOG.md
95
+ - Gemfile
96
+ - Gemfile.lock
97
+ - LICENSE.txt
98
+ - README.md
99
+ - Rakefile
100
+ - bin/console
101
+ - bin/itch-rewards
102
+ - bin/setup
103
+ - itch_rewards.gemspec
104
+ - lib/itch_rewards.rb
105
+ - lib/itch_rewards/cli.rb
106
+ - lib/itch_rewards/templates/reward_config.yml.erb
107
+ - lib/itch_rewards/version.rb
108
+ homepage: https://github.com/Billiam/itch-community-rewards
109
+ licenses:
110
+ - MIT
111
+ metadata:
112
+ homepage_uri: https://github.com/Billiam/itch-community-rewards
113
+ source_code_uri: https://github.com/Billiam/itch-community-rewards
114
+ changelog_uri: https://github.com/Billiam/itch-community-rewards/CHANGELOG.md
115
+ post_install_message:
116
+ rdoc_options: []
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: 2.7.0
124
+ required_rubygems_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ requirements: []
130
+ rubygems_version: 3.2.15
131
+ signing_key:
132
+ specification_version: 4
133
+ summary: Itch community copy automation utility
134
+ test_files: []