Skip to content
This repository has been archived by the owner on Dec 14, 2024. It is now read-only.

Commit

Permalink
Add clone command. Close #19
Browse files Browse the repository at this point in the history
Add support for gym. Close #22
Fix languages dependency of pulling codes.
  • Loading branch information
xalanq committed Jul 23, 2019
1 parent ce9a806 commit 589e4cf
Show file tree
Hide file tree
Showing 17 changed files with 367 additions and 81 deletions.
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,16 @@ It's fast, small, cross-platorm and powerful.

## Features

* Submit codes to a problem of a contest.
* Support all programming languages in Codeforces.
* Support contests and gym in Codeforces.
* Submit codes.
* Watch submissions' status dynamically.
* List all problems' stats of a contest.
* Fetch all problems' samples of a contest (parallel).
* Fetch the latest or "Accepted" codes of a contest.
* Generate a code from the specified template (including timestamp, author, etc.)
* Test samples and feedback.
* Use default web browser to open problems, the standing page.
* Support all programming languages in codeforces.
* Fetch problems' samples.
* Build, test and run.
* Clone all codes of someone.
* Generate codes from the specified template (including timestamp, author, etc.)
* List problems' stats.
* Use default web browser to open problems' pages, standings' page, etc.
* Colorful CLI.

Pull requests are always welcome.
Expand Down Expand Up @@ -62,6 +63,7 @@ Usage:
cf sid [<submission-id>] [<contest-id>]
cf race <contest-id>
cf pull [ac] [<contest-id>] [<problem-id>]
cf clone [ac] <username>
cf upgrade
Examples:
Expand Down Expand Up @@ -98,6 +100,7 @@ Examples:
cf pull 100 a Pull the latest code of problem "a" of contest 100 into "./100/<problem-id>".
cf pull ac 100 a Pull the "Accepted" or "Pretests passed" code of problem "a" of contest 100.
cf pull Pull the latest code of current problem into current path.
cf clone xalanq Clone all codes of xalanq.
cf upgrade Upgrade the "cf" to the latest version from GitHub.
Notes:
Expand Down
21 changes: 12 additions & 9 deletions README_zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ Codeforces Tool 是 [Codeforces](https://codeforces.com) 的命令行界面的

## 特点

* 提交代码到某场比赛的某道题目
* 查看提交后的情况(动态刷新)
* 支持 Codeforces 中的所有编程语言
* 支持 Codeforces 的 contests 和 gym
* 提交代码
* 动态刷新提交后的情况
* 拉取问题的样例
* 构建,测试和运行
* 拉取某人的所有代码
* 从指定模板生成代码(包括时间戳,作者等)
* 列出某场比赛的所有题目的整体信息
* 并行地获取某场比赛所有题目(或者某道题)的样例
* 获取某场比赛所有的最新代码或者AC代码
* 根据你事先准备好的模板代码,生成一份带有时间戳、作者等信息的代码
* 全自动测试样例是否通过,若有错还会给出对比信息
* 用默认的网页浏览器打开题目页面、榜单
* 支持 codeforces 上全部的编程语言
* 不是黑白的命令行界面
* 用默认的网页浏览器打开题目页面、榜单。
* 丰富多彩的CLI。

欢迎大家一起完善这个工具呀,欢迎Pull requests。

Expand Down Expand Up @@ -60,6 +61,7 @@ $ go build -ldflags "-s -w" cf.go
cf sid [<submission-id>] [<contest-id>]
cf race <contest-id>
cf pull [ac] [<contest-id>] [<problem-id>]
cf clone [ac] <username>
cf upgrade
例子:
Expand Down Expand Up @@ -94,6 +96,7 @@ $ go build -ldflags "-s -w" cf.go
cf pull 100 a 拉取比赛 id 为 100 的题目 a 的最新代码到文件夹 "./100/a" 下。
cf pull ac 100 a 拉取比赛 id 为 100 的题目 a 的 AC 代码。
cf pull 拉取当前题目的最新代码到当前文件夹下。
cf clone xalanq 拉取 xalanq 的所有提交代码。
cf upgrade 从 GitHub 更新 "cf" 到最新版。
注意:
Expand Down
4 changes: 3 additions & 1 deletion cf.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
docopt "github.com/docopt/docopt-go"
)

const version = "v0.6.1"
const version = "v0.7.0"

func main() {
usage := `Codeforces Tool $%version%$ (cf). https://github.com/xalanq/cf-tool
Expand All @@ -34,6 +34,7 @@ Usage:
cf sid [<submission-id>] [<contest-id>]
cf race <contest-id>
cf pull [ac] [<contest-id>] [<problem-id>]
cf clone [ac] <username>
cf upgrade
Examples:
Expand Down Expand Up @@ -70,6 +71,7 @@ Examples:
cf pull 100 a Pull the latest code of problem "a" of contest 100 into "./100/<problem-id>".
cf pull ac 100 a Pull the "Accepted" or "Pretests passed" code of problem "a" of contest 100.
cf pull Pull the latest code of current problem into current path.
cf clone xalanq Clone all codes of xalanq.
cf upgrade Upgrade the "cf" to the latest version from GitHub.
Notes:
Expand Down
9 changes: 7 additions & 2 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package client
import (
"encoding/json"
"io/ioutil"
"net/http"
"os"

"github.com/fatih/color"
Expand All @@ -17,13 +18,17 @@ type Client struct {
Bfaa string `json:"bfaa"`
LastSubmission *SaveSubmission `json:"last_submission"`
path string
client *http.Client
}

// New client
func New(path string) *Client {
jar, _ := cookiejar.New(nil)
c := &Client{Jar: jar, path: path, LastSubmission: nil}
c.load()
c := &Client{Jar: jar, LastSubmission: nil, path: path, client: nil}
if path != "" {
c.load()
}
c.client = &http.Client{Jar: c.Jar}
return c
}

Expand Down
170 changes: 170 additions & 0 deletions client/clone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package client

import (
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"sync"
"time"

"github.com/fatih/color"
"github.com/xalanq/cf-tool/cookiejar"
)

type cloneData struct {
contestID string
submissionID string
path string
ext string
}

// Clone all ac codes of all contests
func (c *Client) Clone(username, rootPath string, ac bool) (err error) {
color.Cyan("Clone codes of %v, ac: %v", username, ac)

jar, _ := cookiejar.New(nil)
if username == c.Username {
resp, err := c.client.Get("https://codeforces.com")
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}

if err = checkLogin(c.Username, body); err != nil {
return err
}
jar = c.Jar.Copy()
}

resp, err := c.client.Get(fmt.Sprintf("https://codeforces.com/api/user.status?handle=%v", username))
if err != nil {
return
}
defer resp.Body.Close()
decoder := json.NewDecoder(resp.Body)
var data map[string]interface{}
if err = decoder.Decode(&data); err != nil {
return
}

c.Jar = jar

if status, ok := data["status"].(string); !ok || status != "OK" {
return fmt.Errorf("Cannot get any submission")
}
submissions := data["result"].([]interface{})
total := len(submissions)
count := 0
color.Cyan("Total submission: %v", total)

threadNumber := 16
ch := make(chan cloneData, threadNumber)
again := make(chan cloneData, threadNumber)
wg := sync.WaitGroup{}
wg.Add(threadNumber + 1)
mu := sync.Mutex{}

go func() {
for {
s, ok := <-again
if !ok {
wg.Done()
return
}
ch <- s
}
}()

for gid := 0; gid < threadNumber; gid++ {
go func() {
for {
s, ok := <-ch
if !ok {
wg.Done()
return
}
filename, err := c.PullCode(
s.contestID,
s.submissionID,
s.path,
s.ext,
false,
)
if err == nil {
mu.Lock()
count++
color.Green(fmt.Sprintf(`%v/%v Saved %v`, count, total, filename))
mu.Unlock()
} else {
if username == c.Username {
err = fmt.Errorf("Too many requests")
}
if err.Error() == "Too many requests" {
mu.Lock()
count++
const WAIT int = 500
color.Red(fmt.Sprintf(`%v/%v Error in %v|%v: %v. Waiting for %v seconds to continue.`,
count, total, s.contestID, s.submissionID, err.Error(), WAIT))
mu.Unlock()
time.Sleep(time.Duration(WAIT) * time.Second)
mu.Lock()
count--
mu.Unlock()
again <- s
} else {
mu.Lock()
count++
color.Red(fmt.Sprintf(`%v/%v Error in %v|%v: %v`, count, total, s.contestID, s.submissionID, err.Error()))
mu.Unlock()
}
}
}
}()
}
for _, _submission := range submissions {
submission := _submission.(map[string]interface{})
verdict := submission["verdict"].(string)
contestID := fmt.Sprintf("%v", int64(submission["contestId"].(float64)))
submissionID := fmt.Sprintf("%v", int64(submission["id"].(float64)))
if ac && verdict != "OK" {
mu.Lock()
count++
color.Green(fmt.Sprintf(`%v/%v Skip %v|%v: Not an accepted code`, count, total, contestID, submissionID))
mu.Unlock()
continue
}
lang := submission["programmingLanguage"].(string)
ext, ok := LangsExt[lang]
if !ok {
mu.Lock()
count++
color.Red(fmt.Sprintf(`%v/%v Error in %v|%v: Language "%v" is not supported`, count, total, contestID, submissionID, lang))
mu.Unlock()
continue
}
problemID := strings.ToLower(submission["problem"].(map[string]interface{})["index"].(string))
filename := submissionID
if verdict != "OK" {
testCount := int64(submission["passedTestCount"].(float64))
filename = fmt.Sprintf("%v_%v_%v", submissionID, strings.ToLower(verdict), testCount)
}
which := "contest"
if len(contestID) >= 6 {
which = "gym"
}
path := filepath.Join(rootPath, username, which, contestID, problemID, filename)
data := cloneData{contestID, submissionID, path, "." + ext}
ch <- data
}
close(ch)
close(again)
wg.Wait()

return nil
}
19 changes: 19 additions & 0 deletions client/langs.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ var Langs = map[string]string{
var LangsExt = map[string]string{
"GNU C11": "c",
"Clang++17 Diagnostics": "cpp",
"GNU C++0x": "cpp",
"GNU C++": "cpp",
"GNU C++11": "cpp",
"GNU C++14": "cpp",
"GNU C++17": "cpp",
Expand All @@ -115,4 +117,21 @@ var LangsExt = map[string]string{
"Rust": "rs",
"JavaScript": "js",
"Node.js": "js",
"Q#": "qs",
"Java": "java",
"Java 6": "java",
"Java 7": "java",
"Java 8": "java",
"Java 9": "java",
"Tcl": "tcl",
"F#": "fs",
"Befunge": "bf",
"Pike": "pike",
"Io": "io",
"Factor": "factor",
"Cobol": "cbl",
"Secret_171": "secret_171",
"Ada": "adb",
"FALSE": "f",
"": "txt",
}
6 changes: 3 additions & 3 deletions client/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ func (c *Client) Login(username, password string) (err error) {
jar, _ := cookiejar.New(nil)
color.Cyan("Login %v...\n", username)

client := &http.Client{Jar: jar}
c.client = &http.Client{Jar: jar}

resp, err := client.Get("https://codeforces.com/enter")
resp, err := c.client.Get("https://codeforces.com/enter")
if err != nil {
return
}
Expand All @@ -69,7 +69,7 @@ func (c *Client) Login(username, password string) (err error) {
ftaa := genFtaa()
bfaa := genBfaa()

resp, err = client.PostForm("https://codeforces.com/enter", url.Values{
resp, err = c.client.PostForm("https://codeforces.com/enter", url.Values{
"csrf_token": {csrf},
"action": {"enter"},
"ftaa": {ftaa},
Expand Down
18 changes: 13 additions & 5 deletions client/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ import (
"github.com/fatih/color"
)

// ToGym if length of contestID >= 6, replace contest to gym
func ToGym(URL, contestID string) string {
if len(contestID) >= 6 {
URL = strings.Replace(URL, "contest", "gym", -1)
}
return URL
}

func findSample(body []byte) (input [][]byte, output [][]byte, err error) {
irg := regexp.MustCompile(`class="input"[\s\S]*?<pre>([\s\S]*?)</pre>`)
org := regexp.MustCompile(`class="output"[\s\S]*?<pre>([\s\S]*?)</pre>`)
Expand All @@ -36,9 +44,9 @@ func findSample(body []byte) (input [][]byte, output [][]byte, err error) {
}

// ParseProblem parse problem to path
func (c *Client) ParseProblem(problemURL, path string) (samples int, err error) {
client := &http.Client{Jar: c.Jar}
resp, err := client.Get(problemURL)
func (c *Client) ParseProblem(URL, path string) (samples int, err error) {
client := &http.Client{Jar: c.Jar.Copy()}
resp, err := client.Get(URL)
if err != nil {
return
}
Expand Down Expand Up @@ -79,8 +87,8 @@ func (c *Client) ParseContestProblem(contestID, problemID, path string) (samples
if err != nil {
return
}
problemURL := fmt.Sprintf("https://codeforces.com/contest/%v/problem/%v", contestID, problemID)
samples, err = c.ParseProblem(problemURL, path)
URL := ToGym(fmt.Sprintf("https://codeforces.com/contest/%v/problem/%v", contestID, problemID), contestID)
samples, err = c.ParseProblem(URL, path)
if err != nil {
return
}
Expand Down
Loading

0 comments on commit 589e4cf

Please sign in to comment.