Неофициальная Go-библиотека для сервисов Университета ИТМО: my.itmo, БАРС и ITMO.ID
Покрытие: все 452 маршрута личного кабинета my.itmo.ru (809 методов, включая служебные разделы), все 81 маршрут REST API БАРС (91 метод) и полный цикл входа ITMO.ID.
go get github.com/kewldan/go-itmo@latestНужен Go 1.27+. Зависимости: только golang.org/x/oauth2 и golang.org/x/net.
| Пакет | Что делает |
|---|---|
itmoid |
Вход в ITMO.ID (Keycloak): логин/пароль, одноразовый код (OTP), SSO без пароля, PKCE, обновление и хранение токенов, выход, userinfo, разбор ID-токена, impersonation |
myitmo |
Клиент my.itmo.ru: все разделы кабинета — студенческие и служебные — плюс QR-пропуск qr.itmo.su |
bars |
Клиент БАРС: собственная сессия, тихое продление через SSO, выбор периода, журналы, оценки, планы контрольных точек, отчёты |
ctx := context.Background()
auth := itmoid.New()
tok, err := auth.Login(ctx, itmoid.MyITMO, itmoid.Credentials{
Username: os.Getenv("ITMO_LOGIN"),
Password: os.Getenv("ITMO_PASSWORD"),
})
if err != nil {
log.Fatal(err)
}
// ITMO.ID ротирует refresh token при каждом обновлении — сохраняйте каждый новый.
save := func(t *oauth2.Token) { _ = os.WriteFile(".refresh-token", []byte(t.RefreshToken), 0o600) }
save(tok)
client := myitmo.New(auth.TokenSource(ctx, itmoid.MyITMO, tok, save))
days, err := client.Schedule.Personal(ctx, myitmo.Today(), myitmo.Today().AddDays(7))Authenticator.Login проходит форму ITMO.ID так же, как браузер: получает
страницу входа, отправляет логин и пароль, забирает код из редиректа и
обменивает его на токены (для my.itmo — с PKCE). Логин и пароль не хранятся.
Ошибки различимы через errors.Is:
| Ошибка | Когда |
|---|---|
itmoid.ErrInvalidCredentials |
неверный логин/пароль или OTP; текст с формы — в *itmoid.FormError |
itmoid.ErrOTPRequired |
включена двухфакторка, а Credentials.OTP не задан |
itmoid.ErrLoginRequired |
вход без пароля (SSO), но сессии ITMO.ID нет |
itmoid.ErrUnexpectedPage |
ITMO.ID показал страницу, которую нужно пройти в браузере (смена пароля и т. п.) |
tok, err := auth.Login(ctx, itmoid.MyITMO, itmoid.Credentials{
Username: login, Password: password,
OTP: func(ctx context.Context) (string, error) { return askUser("Код из приложения") },
})ts := auth.FromRefreshToken(ctx, itmoid.MyITMO, storedRefreshToken, saveToken)
client := myitmo.New(ts)itmoid.RefreshExpiry(tok) говорит, когда истечёт refresh token;
itmoid.IDClaims(tok) возвращает ISU, ФИО и фото из ID-токена (без проверки
подписи — только для отображения).
Если пароль не должен попадать в приложение, покажите пользователю настоящую страницу ITMO.ID и перехватите редирект на callback:
state, verifier := itmoid.NewState(), oauth2.GenerateVerifier()
loginURL := auth.AuthCodeURL(itmoid.MyITMO, state, verifier) // открыть в WebView
// WebView перешёл на https://my.itmo.ru/login/callback?state=...&code=...
code, err := itmoid.ParseCallback(itmoid.MyITMO, auth.Issuer(), redirectURL, state)
tok, err := auth.Exchange(ctx, itmoid.MyITMO, &itmoid.Code{Value: code, Verifier: verifier, State: state})ParseCallback строго проверяет схему, хост, путь, state, iss, отсутствие
error, фрагмента и повторяющихся параметров. Для БАРС используйте
itmoid.BARS (без PKCE, verifier пустой) и передайте код в bars.Client.Login.
WebView перехватывает редирект до загрузки страницы. В обычном браузере так
сделать нельзя: браузер откроет https://my.itmo.ru/login/callback, и скрипт
my.itmo сам потратит одноразовый код. Обмен в приложении тогда падает с
invalid_grant: Code not valid.
ITMO.ID принимает для student-personal-cabinet любой адрес вида
https://my.itmo.ru/*. Поэтому редирект можно направить на статический файл,
где скриптов нет, и попросить пользователя скопировать адрес из адресной строки:
app := itmoid.MyITMO
app.RedirectURL = "https://my.itmo.ru/robots.txt" // простой текст, код никто не тронет
loginURL := auth.AuthCodeURL(app, state, verifier) // показать пользователю или открыть в браузере
// пользователь вставляет https://my.itmo.ru/robots.txt?state=...&code=...
code, err := itmoid.ParseCallback(app, auth.Issuer(), pastedURL, state)
tok, err := auth.Exchange(ctx, app, &itmoid.Code{Value: code, Verifier: verifier, State: state})
client := myitmo.New(auth.TokenSource(ctx, itmoid.MyITMO, tok, save))RedirectURL в Exchange должен совпадать с адресом из AuthCodeURL. Для
обновления токена адрес не нужен, так что дальше можно передавать обычный
itmoid.MyITMO. Код живёт около минуты.
Например, скопированный из браузера. Он живёт несколько минут и сам не обновляется:
client := myitmo.New(oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}))| Метод | Что делает |
|---|---|
auth.Refresh(ctx, app, refreshToken) |
принудительно обновить токены |
auth.Logout(ctx, app, refreshToken) |
завершить SSO-сессию ITMO.ID |
auth.UserInfo(ctx, ts) |
OIDC userinfo: ISU, ФИО, почта |
itmoid.IDClaims(tok) |
те же данные из ID-токена без запроса (подпись не проверяется) |
itmoid.RefreshExpiry(tok) |
когда истечёт refresh token |
auth.Impersonate(ctx, app, accessToken, subject) |
войти от имени другого пользователя (RFC 8693); только для аккаунтов поддержки |
itmoid.MyITMODev |
стенд dev.my.itmo.su (вместе с myitmo.WithBaseURL(myitmo.DevBaseURL)) |
Authenticator хранит SSO-cookie ITMO.ID. После входа в my.itmo тот же
Authenticator получает код для БАРС без пароля:
barsClient := bars.New(bars.WithAuthenticator(auth))
err := barsClient.LoginSSO(ctx)Сервисы сгруппированы так же, как разделы кабинета:
client.Schedule.Personal(ctx, from, to) // расписание
client.RecordBook.Specializations(ctx) // зачётка
client.Sport.Schedule(ctx, myitmo.SportScheduleParams{DateStart: from, DateEnd: to}) // спорт
client.Personalities.SearchAll(ctx, "Иванов", 50) // iter.Seq2 по всем страницам
client.QR.Pass(ctx) // QR-пропуск| Поле клиента | Раздел кабинета | Маршруты | Методов | Кому |
|---|---|---|---|---|
Schedule |
Расписание | /api/schedule |
3 | все |
RecordBook |
Зачётка | /api/record_book |
3 | студент |
StudyPlan |
Учебный план, выбор и замена дисциплин | /api/eduPlanNew, /api/eduPlan |
9 | студент |
IndividualPlan |
Индивидуальные планы | /api/individual-plan |
35 | сотрудник |
Election |
Выборность дисциплин и потоков | /api/election |
15 | студент |
Intro, Facultative |
Вводные курсы и факультативы | /api/intro, /api/facultative |
13 | студент |
Sport |
Физкультура: запись, баллы, секции, соревнования | /api/sport |
34 | студент |
Personalities |
Персоналии | /api/personalities |
4 | все |
System |
Главный экран, меню, сервисы | /api/system |
3 | все |
Requests |
Заявки (классические) | /api/requests |
12 | все |
RequestsV2 |
Заявки на процессах, конструктор форм и процессов | /api/requests/v2 |
58 | все / админ |
Agreements |
Согласия и ознакомления | /api/agreements |
4 | все |
Sign |
Электронная подпись (простая, Контур, Госключ) | /api/sign |
11 | все |
Assistant |
ИИ-ассистент (SSE-стрим) | /api/assistant |
7 | все |
Finances |
Стипендии, оплата обучения, зарплата | /api/finances |
13 | студент / сотрудник |
Dormitory |
Общежитие: заселение, договоры, оплата | /api/dormitory |
11 | студент |
DMS |
ДМС | /api/dms |
13 | все / HR |
Booking |
Бронирование аудиторий | /api/booking |
12 | все |
Navigator |
Навигатор по корпусам | /api/navigator |
3 | все |
Queues |
Электронные очереди | /api/queues |
7 | все |
Practices |
Практики и отчёты | /api/practices |
22 | студент |
Employments |
Трудоустройство (HR) | /api/employments |
31 | сотрудник |
GIAStudents |
ГИА и ВКР, приложение к диплому | /api/gia-students |
50 | студент |
GIA |
ГИА: комиссии, защиты, рецензенты, приказы | /api/gia |
175 | сотрудник |
Adviser |
Тьютор: карточка студента | /api/services/adviser |
15 | сотрудник |
Checklist |
Обходной лист | /api/services/checklist |
1 | студент |
Constructor |
Конструктор РПД/РПП/ГИА | /api/constructor |
102 | сотрудник |
ConstructorEP |
Конструктор ОП, банк модулей, КУГ | /api/constructor-ep |
112 | сотрудник |
Vacation |
Отпуска | /api/vacation |
30 | сотрудник |
QR |
QR-пропуск | qr.itmo.su |
1 | все |
Методы служебных разделов (ГИА, конструктор ОП, отпуска, трудоустройство,
администрирование заявок) доступны только аккаунтам с соответствующими правами;
иначе сервер отвечает 403 ((*myitmo.Error).IsForbidden()).
Любая ошибка сервера — *myitmo.Error: HTTP-статус, error_code из ответа
(my.itmo нередко отвечает HTTP 200 с ненулевым кодом) и локализованное
сообщение, которое можно показать пользователю.
_, err := client.Sport.SignIn(ctx, lessonID)
var apiErr *myitmo.Error
if errors.As(err, &apiErr) && apiErr.Code == myitmo.SportErrorCannotSignIn {
fmt.Println(apiErr.Message) // «нельзя записать студента: [...]»
}*myitmo.DecodeError означает, что ответ не совпал с моделью — скорее всего,
сервер изменил тип поля. Пожалуйста, заведите issue.
Если нужного метода ещё нет, client.Do отправит любой запрос с токеном и
разберёт стандартную обёртку {error_code, error_message, result}:
var out []myThing
err := client.Do(ctx, http.MethodGet, "api/some/new/route", nil, nil, &out)client := bars.New(bars.WithAuthenticator(auth), bars.WithStore(myStore))
err := client.LoginPassword(ctx, itmoid.Credentials{Username: login, Password: password})
err = client.WithPeriod(ctx, "2025/2026", bars.Spring, func(ctx context.Context) error {
disciplines, err := client.Disciplines(ctx, true)
// ...
journal, err := client.StudentJournal(ctx, planID, flow.Type, flow.Identifier)
// ...
})Способы входа в БАРС:
| Метод | Когда |
|---|---|
LoginPassword(ctx, creds) |
логин и пароль ITMO.ID (нужен WithAuthenticator) |
LoginSSO(ctx) |
без пароля, если тот же Authenticator уже входил (например, в my.itmo) |
Login(ctx, code) |
код ITMO.ID, полученный сами (WebView, см. выше) |
LoginParent(ctx, login, password) |
родительский аккаунт БАРС (не ITMO.ID) |
RegisterParent(ctx, token, login, password) |
регистрация родителя по ссылке-приглашению студента |
Impersonate(ctx, personalNumber) |
суперпользователь: работать от имени другого пользователя |
WithStore(store) |
сохранить сессию между запусками (по умолчанию — в памяти) |
- Сессия БАРС — заголовок
Bearer ..., живёт ~30 минут и не обновляется. СWithAuthenticator(илиWithCodeSource) клиент на HTTP 401 один раз получает новый код через SSO и повторяет запрос. - Каталоги и журналы читаются в контексте периода, сохранённого на сервере и
общего для всех сессий пользователя.
WithPeriodпереключает период и не даёт другим вызовамWithPeriodэтого клиента сменить его, пока выполняется функция. StudentMarks.Totalу пустого журнала равен 0 — проверяйтеHasAnyMark().Approval.GradeCode()переводит «Удвл., E» в формат my.itmo «3/E».- Идентификаторы БАРС не совпадают с
discipline_idиest_idmy.itmo.
client.QR.Pass(ctx) возвращает строку Hex. QR-код кодирует сам этот текст
(byte mode, уровень коррекции L), например с github.com/skip2/go-qrcode или
rsc.io/qr.
- Токены отправляются только на хосты my.itmo и qr.itmo.su, даже если в
Doпередан абсолютный URL на другой хост. - Логирование (
WithLogger) пишет только метод, путь, статус и время ответа — без токенов, query-параметров и тел. - Ошибки БАРС не содержат тела ответа.
- Не храните пароль. Храните refresh token так же бережно, как пароль
(
0600, системное хранилище секретов).
Поля, форма которых ещё не подтверждена, имеют тип RawJSON — их можно
разобрать самостоятельно. Если ответ сервера не совпал с моделью, придёт
*myitmo.DecodeError; пришлите issue или PR с обезличенным примером.
make build # go build + go vet
make test # go test -race ./...
make lint # golangci-lint v2Правила для контрибьюторов (и агентов) — в AGENTS.md.
- CI (
ci.yml): gofmt иgo mod tidy, сборка,go vet, тесты с-raceна Linux, macOS и Windows, отчёт о покрытии, golangci-lint, govulncheck. - CodeQL (
codeql.yml): статический анализ безопасности, также раз в неделю. - Release (
release.yml): на тегvX.Y.Z— тесты, GitHub Release с автоматическими заметками и регистрация версии в proxy.golang.org. - Dependabot: еженедельные обновления Go-модулей и GitHub Actions.
Проект не связан с Университетом ИТМО. Используйте только со своей учётной записью и в рамках правил университета; служебные методы — только при наличии соответствующих прав.