10 Commits
9 changed files with 302 additions and 85 deletions

No files matched your search

+35
View File
@@ -0,0 +1,35 @@
import json
import os
import uuid
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
server = 'https://git.ermnvldmr.com/api/v1/repos/' + os.environ['GITHUB_REPOSITORY']
headers = {'Authorization': 'token ' + os.environ['RELEASE_TOKEN']}
tag = os.environ['GITHUB_REF_NAME']
def request(path, data=None, content_type=None):
current = headers.copy()
if content_type:
current['Content-Type'] = content_type
req = urllib.request.Request(server + path, data=data, headers=current)
with urllib.request.urlopen(req, timeout=120) as response:
return json.load(response)
try:
release = request('/releases/tags/' + urllib.parse.quote(tag, safe=''))
except urllib.error.HTTPError as error:
if error.code != 404:
raise
release = request('/releases', json.dumps({'tag_name': tag, 'name': tag, 'draft': False}).encode(), 'application/json')
for artifact in sorted(Path('artifacts').glob('*')):
if not artifact.is_file():
continue
boundary = uuid.uuid4().hex
filename = artifact.name.replace('"', '_').replace('\r', '_').replace('\n', '_')
body = (f'--{boundary}\r\nContent-Disposition: form-data; name="attachment"; filename="{filename}"\r\nContent-Type: application/octet-stream\r\n\r\n'.encode() + artifact.read_bytes() + f'\r\n--{boundary}--\r\n'.encode())
request('/releases/' + str(release['id']) + '/assets?name=' + urllib.parse.quote(filename), body, 'multipart/form-data; boundary=' + boundary)
+70
View File
@@ -0,0 +1,70 @@
name: actions
'on':
push:
tags:
- v*.*.*
jobs:
pkg:
runs-on: apex-debian13
strategy:
matrix:
node-version:
- 18.x
pnpm-version:
- 8
steps:
- name: Check out the repo
run: "set -euo pipefail\ncase \"$CHECKOUT_REF\" in -*) exit 1;; esac\nGITHUB_SERVER_URL=${GITHUB_SERVER_URL%/}\n\
case \"$GITHUB_SERVER_URL\" in https://git.ermnvldmr.com) ;; *) exit 1;; esac\n\
AUTH=$(printf 'deytenit:%s' \"$CHECKOUT_TOKEN\" | base64 -w0)\nHEADER=\"http.${GITHUB_SERVER_URL}/.extraheader=AUTHORIZATION:\
\ basic $AUTH\"\ngit init \"$GITHUB_WORKSPACE\"\ncd \"$GITHUB_WORKSPACE\"\n\
git remote add origin \"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git\"\ngit -c\
\ \"$HEADER\" fetch --tags origin '+refs/heads/*:refs/remotes/origin/*'\n\
git -c \"$HEADER\" fetch origin \"$CHECKOUT_REF\"\ngit checkout --detach FETCH_HEAD\n\
test \"$(git rev-parse HEAD)\" = \"$GITHUB_SHA\"\nif [ \"$CHECKOUT_SUBMODULES\"\
\ = recursive ]; then\n key_file=$(mktemp \"${RUNNER_TEMP:-/tmp}/hextra-key.XXXXXX\"\
)\n trap 'rm -f \"$key_file\"' EXIT\n umask 077\n printf '%s\\n'\
\ \"$HEXTRA_READ_KEY\" > \"$key_file\"\n chmod 600 \"$key_file\"\n export\
\ GIT_SSH_COMMAND=\"ssh -T -i $key_file -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes\
\ -o HostKeyAlgorithms=ssh-ed25519 -o UserKnownHostsFile=$GITHUB_WORKSPACE/.gitea/known_hosts\"\
\n git submodule update --init --recursive\nfi\nif [ \"$PERSIST_CREDENTIALS\"\
\ = true ]; then\n git config \"http.${GITHUB_SERVER_URL}/.extraheader\"\
\ \"AUTHORIZATION: basic $AUTH\"\nfi\n"
env:
CHECKOUT_REF: ${{ github.sha }}
CHECKOUT_SUBMODULES: 'false'
CHECKOUT_TOKEN: ${{ github.token }}
PERSIST_CREDENTIALS: 'false'
HEXTRA_READ_KEY: ''
- name: Use Node.js ${{ matrix.node-version }}
run: "case \"$NODE_VERSION\" in\n 18*) major=18;; 20*) major=20;; 22*) major=22;;\
\ *) exit 1;;\nesac\ntest -x \"/opt/ci/node/$major/bin/node\"\necho \"/opt/ci/node/$major/bin\"\
\ >> \"$GITHUB_PATH\""
env:
NODE_VERSION: ${{ matrix.node-version }}
- name: Install PKG
run: 'test -x /opt/ci/pkg/bin/pkg
echo /opt/ci/pkg/bin >> "$GITHUB_PATH"'
- name: Install dependencies
run: npm install
- name: ESLint & Prettier
run: npm run lint
- name: Build
run: npm run build
- name: Test
run: npm run --if-present test
- name: Package
run: pkg --out-path ./artifacts ./dist/index.js
- name: Naming
run: for f in ./artifacts/index*; do mv -i -- "$f" "${f//index/styleforces}";
done
- name: Release
if: startsWith(github.ref, 'refs/tags/')
run: python3 .gitea/scripts/release.py
env:
RELEASE_TOKEN: ${{ github.token }}
env:
PKG_CACHE_PATH: /opt/ci/pkg-cache
permissions:
contents: write
-53
View File
@@ -1,53 +0,0 @@
name: actions
on:
push:
tags:
- "v*.*.*"
jobs:
pkg:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
pnpm-version: [8]
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install PKG
run: npm install -g pkg
- name: Install dependencies
run: npm install
- name: ESLint & Prettier
run: npm run lint
- name: Build
run: npm run build
- name: Test
run: npm run --if-present test
- name: Package
run: pkg --out-path ./artifacts ./dist/index.js
- name: Naming
run: for f in ./artifacts/index*; do mv -i -- "$f" "${f//index/styleforces}"; done
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
artifacts/*
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023-present Vladimir Eremin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+101
View File
@@ -0,0 +1,101 @@
# StyleForces
The Codeforces toolkit to make life easier for teachers and worse for students.
> One year I made one thing, next year another. They're small but still useful, so I've bundled them.
## Commands
One binary contains multiple commands, here is the listing.
### log2StanD
Convert contest log to JSON with freeze-time.
```cs
Usage: styleforces log2StanD [options...]
Options:
--freeze, -f (INTEGER) : Freeze time counting from start in minutes.
--log, -l (STRING) : Path to the contest log file.
--domain, -d (STRING) : Path to the domain's user's file.
--only-domain, -i : Ignore everyone not in domain.
--out-file, -O <STRING> : Path to the resulted serialization. (default: './result.json')
Example: styleforces log2StanD -f 120 -l ./log.txt -d ./domain.txt -o ./result.json
```
> This one was used on a local team Olympiad to make it more exciting for participants.
>
> ACM ICPC like experience powered by [StanD](https://github.com/OStrekalovsky/S4RiS-StanD).
### fetch
Fetch contest submissions ("OK" or point-threshold).
```cs
Usage: styleforces fetch [options...]
Options:
--contest, -i (STRING) : Contest ID.
--group, -g <STRING> : Group ID (if in group).
--count, -c <INTEGER> : Positive amount of submissions to fetch. (default: 100)
--lookup, -l <INTEGER> : Positive amount of submissions to lookup. (default: 200)
--after, -a <INTEGER> : Positive amount of submissions to skip. (default: 1)
--points, -p <INTEGER> : Positive amount of points to filter. (default: 100)
--exclude, -e [STRING] : Index of problems to exclude from fetching.
--out-dir, -O <STRING> : Path to the directory to write submissions to. (default: './result')
Example: styleforces fetch -i someId -g anotherId -c 50 -l 100 -a 10 -o './result'
```
> Basically, you do this to make some cool stuff later.
### copydetect
Copydetect wrapper compatible with fetch command.
```cs
Usage: styleforces copydetect [copydetect options...]
Example: styleforces copydetect -t ./data -o cpp -O ./result.html
```
> ⚠️ [copydetect](https://github.com/blingenf/copydetect) needs to be accessible in your environment!
> This makes fabulous HTML copy paste reports over submissions. Easily catch cheaters in your contests.
## Enviroment
More about usage!
### Variables
- **CF_KEY** - Codeforces API Key.
- **CF_SECRET** - Codeforces API Secret.
- **CF_COOKIE** - Cookies extracted from your session.
> ❔About cookies: some info isn't accessible through codeforces API, though we need to scrap HTML to access it.
> ❕Moreover contests require **manager** role to access participant's submissions.
### Options
- **--help, -h** - should work everywhere depending on context. **Default:**
```cs
Usage: styleforces (command) [arguments...]
Commands:
- log2StanD : Convert contest log to JSON with freeze-time.
- fetch : Fetch submission from contest.
- copydetect : Compare submissions and generate copy report of them. (requires copydetect)
Environment:
CF_KEY : Codeforces API Key.
CF_SECRET : Codeforces API Secret.
CF_COOKIE : Cookies extracted from your session.
```
## Bundles
> Maybe later...
## Postface
**[Licence](https://github.com/unknowableshade/StyleForces/blob/master/LICENCE)**
**_Thank you for your attention!_**
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "styleforces",
"version": "2.0.1",
"version": "2.0.3",
"description": "The Codeforces toolkit to make life easier for teachers and worse for students.",
"main": "./dist/index.js",
"scripts": {
@@ -11,16 +11,18 @@ type SubmissionIndex = {
};
export const HELP = `
Usage: ${process.execPath} fetchOk [options...]
Usage: ${process.execPath} fetch [options...]
Options:
--contest, -i (STRING) : Contest ID.
--group, -g <STRING> : Group ID (if in group).
--count, -c <INTEGER> : Positive amount of "OK" submissions to fetch. (default: 100)
--count, -c <INTEGER> : Positive amount of submissions to fetch. (default: 100)
--lookup, -l <INTEGER> : Positive amouut of submissions to lookup. (default: 200)
--after, -a <INTEGER> : Positive amount of submissions to skip. (default: 1)
--points, -p <INTEGER> : Positive amount of points to filter. (default: 100)
--exclude, -e [STRING] : Index of problems to exclude from fetching.
--out-dir, -O <STRING> : Path to the directory to write submissions to. (default: './result')
Example: ${process.execPath} fetchOk -i someId -g anotherId -c 50 -l 100 -a 10 -o './result'
Example: ${process.execPath} fetch -i someId -g anotherId -c 50 -l 100 -a 10 -o './result'
`;
function error(msg: string) {
@@ -36,6 +38,8 @@ export async function execute() {
count: countArg,
lookup: lookupArg,
after: afterArg,
points: pointsArg,
exclude: excludeArg,
"out-dir": outDirArg
}
} = parseArgs({
@@ -60,6 +64,15 @@ export async function execute() {
type: "string",
short: "a"
},
points: {
type: "string",
short: "p"
},
exclude: {
type: "string",
short: "e",
multiple: true
},
"out-dir": {
type: "string",
short: "O"
@@ -71,6 +84,8 @@ export async function execute() {
const count = countArg && /^(0|[1-9]\d*)$/.test(countArg) ? parseInt(countArg) : 100;
const lookup = lookupArg && /^(0|[1-9]\d*)$/.test(lookupArg) ? parseInt(lookupArg) : 200;
const after = afterArg && /^(0|[1-9]\d*)$/.test(afterArg) ? parseInt(afterArg) : 1;
const points = pointsArg && /^(0|[1-9]\d*)$/.test(pointsArg) ? parseInt(pointsArg) : 100;
const exclude = new Set<string>(excludeArg);
const outDir = outDirArg ?? "./result";
if (!contestId) {
@@ -95,7 +110,10 @@ export async function execute() {
}
const status = (await codeforces.methods.contestStatus(contestId, after, lookup))
.filter((submission) => submission.verdict === "OK")
.filter(
(submission) =>
submission.verdict === "OK" || (submission.points && submission.points >= points)
)
.slice(0, count);
for (const submission of status) {
@@ -108,7 +126,8 @@ export async function execute() {
(problem) =>
problem.contestId === submission.contestId &&
problem.name === submission.problem.name
)
) ||
exclude.has(submission.problem.index)
) {
continue;
}
@@ -17,14 +17,15 @@ interface Output {
}
export const HELP = `
Usage: ${process.execPath} logSerializer [options...]
Usage: ${process.execPath} log2StanD [options...]
Options:
--freeze, -f (INTEGER) : Freeze time counting from start in minutes.
--lookup, -l (STRING) : Path to the contest log file.
--domain, -d (STRING) : Path to the domain's users file.
--out-file, -O <STRING> : Path to the resulted serialization. (default: './result.json')
--freeze, -f (INTEGER) : Freeze time counting from start in minutes.
--log, -l (STRING) : Path to the contest log file.
--domain, -d (STRING) : Path to the domain's users file.
--only-domain, -i : Ignore everyone not in domain.
--out-file, -O <STRING> : Path to the resulted serialization. (default: './result.json')
Example: ${process.execPath} logSerializer -f 120 -l ./log.txt -d ./domain.txt -o ./result.json
Example: ${process.execPath} log2StanD -f 120 -l ./log.txt -d ./domain.txt -o ./result.json
`;
function error(msg: string) {
@@ -34,7 +35,13 @@ function error(msg: string) {
export async function execute() {
const {
values: { freeze: freezeTime, log: logFile, domain: domainFile, "out-file": outFileArg }
values: {
freeze: freezeTime,
log: logFile,
domain: domainFile,
"only-domain": onlyDomain,
"out-file": outFileArg
}
} = parseArgs({
options: {
freeze: {
@@ -49,6 +56,10 @@ export async function execute() {
type: "string",
short: "d"
},
"only-domain": {
type: "boolean",
short: "i"
},
"out-file": {
type: "string",
short: "O"
@@ -95,6 +106,8 @@ export async function execute() {
runs: []
};
const ignored = new Set<number>();
for (const entry of log) {
const cmd = entry.slice(0, entry.indexOf(" ")).trim();
const csv = entry.slice(entry.indexOf(" ") + 1).trim();
@@ -111,19 +124,30 @@ export async function execute() {
}
case "@t": {
const values = csv.split(",");
output.contestants.push(names[values[3].split("=")[1]]);
const party = /^g\d+?=/.test(values[3]) ? values[3].split("=")[1] : values[3];
if (onlyDomain && !Object.hasOwn(names, party)) {
ignored.add(ignored.size + output.contestants.length);
continue;
}
output.contestants.push(names[party] ?? party);
break;
}
case "@s": {
const values = csv.split(",");
const run: Run = {
contestant: output.contestants[parseInt(values[0]) - 1],
problemLetter: values[1],
timeMinutesFromStart: Math.floor(parseInt(values[3]) / 60),
success: values[4] === "OK"
};
const contestantId = parseInt(values[0]) - 1;
if (!ignored.has(contestantId)) {
const run: Run = {
contestant: output.contestants[contestantId - ignored.size],
problemLetter: values[1],
timeMinutesFromStart: Math.floor(parseInt(values[3]) / 60),
success: values[4] === "OK"
};
output.runs.push(run);
}
output.runs.push(run);
break;
}
}
+11 -11
View File
@@ -1,15 +1,15 @@
import { parseArgs } from "util";
import * as copyDetect from "./command/copydetect.js";
import * as fetchOK from "./command/fetchOk.js";
import * as logSerializer from "./command/logSerializer.js";
import * as fetchSubmissions from "./command/fetch.js";
import * as log2stanD from "./command/log2StanD.js";
const HELP = `
Usage: ... (command) [arguments...]
Commands:
- logSerializer : Convert contest log to JSON with freeze-time.
- fetchOk : Fetch every "OK" submission from contest.
- copydetect : Compare submissions and generate copy report of them. (requires copydetect)
- log2StanD : Convert contest log to JSON with freeze-time.
- fetch : Fetch submission from contest.
- copydetect : Compare submissions and generate copy report of them. (requires copydetect)
Environment:
CF_KEY : Codeforces API Key.
CF_SECRET : Codeforces API Secret.
@@ -30,19 +30,19 @@ const {
});
switch (process.argv[2]) {
case "logSerializer":
case "log2StanD":
if (help) {
console.log(logSerializer.HELP);
console.log(log2stanD.HELP);
break;
}
logSerializer.execute();
log2stanD.execute();
break;
case "fetchOk":
case "fetch":
if (help) {
console.log(fetchOK.HELP);
console.log(fetchSubmissions.HELP);
break;
}
fetchOK.execute();
fetchSubmissions.execute();
break;
case "copydetect":
if (help) {