Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add login #8

Merged
merged 1 commit into from
Jan 15, 2023
Merged
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
17 changes: 17 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ PATH
remote: .
specs:
acclir (0.1.0)
faraday (~> 2.7.2)
faraday-cookie_jar (~> 0.0.7)
minitest
nokogiri (~> 1.13.10)
thor (~> 1.2.1)
Expand All @@ -15,7 +17,18 @@ GEM
crack (0.4.5)
rexml
diff-lcs (1.5.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
faraday (2.7.2)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-net_http (3.0.2)
hashdiff (1.0.1)
http-cookie (1.0.5)
domain_name (~> 0.5)
json (2.6.3)
mini_portile2 (2.8.1)
minitest (5.17.0)
Expand Down Expand Up @@ -61,7 +74,11 @@ GEM
rubocop-ast (1.24.0)
parser (>= 3.1.1.0)
ruby-progressbar (1.11.0)
ruby2_keywords (0.0.5)
thor (1.2.1)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (2.3.0)
webmock (3.18.1)
addressable (>= 2.8.0)
Expand Down
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,47 @@ AtCoder CLI developed in Ruby.

Install the gem and add to the application's Gemfile by executing:

$ bundle add acclir
```
$ bundle add acclir
```

If bundler is not being used to manage dependencies, install the gem by executing:

$ gem install acclir
```
$ gem install acclir
```

## Usage

```console
> acclir help
```
$ acclir help
Acclir commands:
acclir help [COMMAND] # Describe available commands or one specific command
acclir login # Login AtCoder
acclir new CONTEST_ID # Generate files for the contest
```

## Examples

First, login.

```
$ acclir login
```

Prepare for AtCoder Beginner Contest 284.

```console
acclir new abc284
```
$ acclir new abc284
```

`abc284` is the ID assigned to the contest URL.
https://atcoder.jp/contests/abc284

Once the problem is solved, the test can be run.
```console
ruby abc284/abc284_a/main_test.rb

```
$ ruby abc284/abc284_a/main_test.rb
```

## Development
Expand Down
2 changes: 2 additions & 0 deletions acclir.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

spec.add_dependency "faraday", "~> 2.7.2"
spec.add_dependency "faraday-cookie_jar", "~> 0.0.7"
spec.add_dependency "minitest"
spec.add_dependency "nokogiri", "~> 1.13.10"
spec.add_dependency "thor", "~> 1.2.1"
Expand Down
19 changes: 12 additions & 7 deletions lib/acclir/at_coder.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
# frozen_string_literal: true

require "faraday"
require "faraday-cookie_jar"
require "nokogiri"
require "fileutils"

require_relative "./at_coder/connection"
require_relative "./at_coder/error"

require_relative "./at_coder/user"
require_relative "./at_coder/login_session"

require_relative "./at_coder/contest"
require_relative "./at_coder/sample"
require_relative "./at_coder/task"

module Acclir
# AtCoder
module AtCoder
ATCODER_URL = "https://atcoder.jp"

ATCODER_TASKS_PATH_PROC = ->(contest_id) { "/contests/#{contest_id}/tasks" }
ATCODER_TASKS_URL_PROC = ->(contest_id) { "#{ATCODER_URL}/contests/#{contest_id}/tasks" }

ATCODER_TASK_PATH_REGEX_PROC = ->(contest_id) { %r{^/contests/#{contest_id}/tasks/(?<task>\w+)$} }
ATCODER_TASK_URL_PROC = ->(contest_id, task_id) { "#{ATCODER_URL}/contests/#{contest_id}/tasks/#{task_id}" }
end
end
46 changes: 46 additions & 0 deletions lib/acclir/at_coder/connection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

module Acclir
module AtCoder
# Communication with AtCoder
class Connection
ATCODER_ROOT_URL = "https://atcoder.jp"
COOKIE_JAR_FILE_PATH = "/tmp/acclir_cookie.yml"

class << self
def connection
@connection ||= Faraday.new(url: ATCODER_ROOT_URL) do |faraday|
faraday.use :cookie_jar, jar: cookie_jar
faraday.response :raise_error
end
end

def get(path, &block)
connection.get(path, &block)
end

def post(path, save_cookie: false, &block)
FileUtils.rm(COOKIE_JAR_FILE_PATH) if save_cookie

connection.post(path, &block).tap do
save_cookie_jar if save_cookie
end
end

private

def cookie_jar
@cookie_jar ||= if File.exist?(COOKIE_JAR_FILE_PATH)
HTTP::CookieJar.new.load(COOKIE_JAR_FILE_PATH)
else
HTTP::CookieJar.new
end
end

def save_cookie_jar
cookie_jar.save(COOKIE_JAR_FILE_PATH, session: true)
end
end
end
end
end
14 changes: 6 additions & 8 deletions lib/acclir/at_coder/contest.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# frozen_string_literal: true

require "open-uri"

module Acclir
module AtCoder
# AtCoder contest
class Contest
TASKS_PATH_PROC = ->(id) { "/contests/#{id}/tasks" }

attr_reader :id

def initialize(id)
Expand All @@ -18,20 +18,18 @@ def tasks

private

def url
@url ||= ATCODER_TASKS_URL_PROC.call(id)
end

def document
@document ||= Nokogiri::HTML(URI.parse(url).open)
@document ||= Nokogiri::HTML(
Connection.get(TASKS_PATH_PROC.call(id)).body
)
end

def extract_task_ids
hrefs = document.xpath("//a[@href]").map do |element|
element.attribute("href").content
end

hrefs.uniq.map { |href| href.match(ATCODER_TASK_PATH_REGEX_PROC.call(id))&.[](:task) }.compact
hrefs.uniq.map { |href| href.match(Task::PATH_REGEX_PROC.call(id))&.[](:task) }.compact
end
end
end
Expand Down
12 changes: 12 additions & 0 deletions lib/acclir/at_coder/error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module Acclir
module AtCoder
# Login Failed
class LoginFailedError < StandardError
def initialize
super("Login failed. Please try again.")
end
end
end
end
34 changes: 34 additions & 0 deletions lib/acclir/at_coder/login_session.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module Acclir
module AtCoder
# AtCoder session
class LoginSession
PATH = "/login"

class << self
def create(username, password)
response = Connection.post(PATH, save_cookie: true) do |req|
req.headers["Content-Type"] = "application/x-www-form-urlencoded"
req.body =
URI.encode_www_form({ username: username, password: password, csrf_token: csrf_token })
end

raise LoginFailedError if response.headers["location"] == PATH

true
end

private

def csrf_token
Nokogiri::HTML(
Connection.get(PATH).body
).xpath(
"//input[@name='csrf_token']"
).first[:value]
end
end
end
end
end
14 changes: 6 additions & 8 deletions lib/acclir/at_coder/task.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# frozen_string_literal: true

require "open-uri"
require "nokogiri"

module Acclir
module AtCoder
# AtCoder problem
class Task
SAMPLE_INPUT_TITLE = "Sample Input"
SAMPLE_OUTPUT_TITLE = "Sample Output"

PATH_REGEX_PROC = ->(contest_id) { %r{^/contests/#{contest_id}/tasks/(?<task>\w+)$} }
PATH_PROC = ->(contest_id, id) { "/contests/#{contest_id}/tasks/#{id}" }

attr_reader :contest_id, :id

def initialize(contest_id, id)
Expand All @@ -23,12 +23,10 @@ def samples

private

def url
@url ||= ATCODER_TASK_URL_PROC.call(contest_id, id)
end

def document
@document ||= Nokogiri::HTML(URI.parse(url).open)
@document ||= Nokogiri::HTML(
Connection.get(PATH_PROC.call(contest_id, id)).body
)
end

def extract_samples
Expand Down
14 changes: 14 additions & 0 deletions lib/acclir/at_coder/user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module Acclir
module AtCoder
# AtCoder User
class User
class << self
def login(user, password)
LoginSession.create(user, password)
end
end
end
end
end
14 changes: 14 additions & 0 deletions lib/acclir/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ def self.exit_on_failure?
true
end

desc "login", "Login AtCoder"
def login
username = ask("Username:")
password = ask("Password:", echo: false)

begin
AtCoder::User.login(username, password)

say "\nLogin succeeded!"
rescue AtCoder::LoginFailedError => e
say_error "\n#{e.message}"
end
end

register(Command::New, "new", "new CONTEST_ID", "Generate files for the contest")
end
end
26 changes: 26 additions & 0 deletions spec/acclir/at_coder/contest_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

RSpec.describe Acclir::AtCoder::Contest do
describe "#tasks" do
let!(:id) { "abc999" }
let!(:contest) { described_class.new(id) }

before do
stub_request(:get, "https://atcoder.jp/contests/#{id}/tasks").to_return(
body: File.new(File.expand_path("../../fixtures/contest.html", File.dirname(__FILE__))), status: 200
)
end

it "return tasks" do
expect(contest.tasks).to be_truthy
end

it "must be return 2 tasks" do
expect(contest.tasks.size).to eq 2
end

it "must be set task id" do
expect(contest.tasks.map(&:id)).to match_array %w[abc999_a abc999_b]
end
end
end
7 changes: 7 additions & 0 deletions spec/fixtures/contest.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<a href="/not_found"></a>
<a href="/contests/abc999/not_found"></a>

<a href="/contests/abc999/tasks/abc999_a">A</a>

<a href="/contests/abc999/tasks/abc999_b">B</a>
<a href="/contests/abc999/tasks/abc999_b">B</a>