mirror of
https://github.com/elyby/chrly.git
synced 2024-08-26 06:40:02 +05:30
Merge branch 'v4'
This commit is contained in:
commit
d1b1f22a93
@ -1,2 +1,5 @@
|
|||||||
# Игнорим данные, т.к. они не нужны для внутреннего содержимого этого контейнера
|
# Игнорим данные, т.к. они не нужны для внутреннего содержимого этого контейнера
|
||||||
data
|
data
|
||||||
|
|
||||||
|
# Vendor так же не нужен
|
||||||
|
vendor
|
||||||
|
13
.gitignore
vendored
13
.gitignore
vendored
@ -2,5 +2,14 @@
|
|||||||
/.idea
|
/.idea
|
||||||
|
|
||||||
# Docker Compose file
|
# Docker Compose file
|
||||||
docker-compose.yml
|
/docker-compose.yml
|
||||||
docker-compose.override.yml
|
/docker-compose.override.yml
|
||||||
|
|
||||||
|
# vendor
|
||||||
|
/vendor
|
||||||
|
|
||||||
|
# Cover output
|
||||||
|
.cover
|
||||||
|
|
||||||
|
# Local config
|
||||||
|
/config.yml
|
||||||
|
@ -1,33 +1,96 @@
|
|||||||
image: docker:latest
|
# Предполагается, что между работой "build docker container" и этапом push
|
||||||
|
# построенные docker images остаются статичными и никуда не пропадают
|
||||||
|
#
|
||||||
|
# В противном случае их нужно после каждого этапа билда пушить в registry
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
|
- test
|
||||||
- build
|
- build
|
||||||
|
- build_docker_image
|
||||||
- push
|
- push
|
||||||
|
- cleanup
|
||||||
before_script:
|
|
||||||
- docker login -u gitlab-ci -p $CI_BUILD_TOKEN registry.ely.by
|
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
CONTAINER_IMAGE: registry.ely.by/elyby/skinsystem
|
CONTAINER_IMAGE: registry.ely.by/elyby/skinsystem
|
||||||
|
|
||||||
build:
|
.golang_template: &setup_go_environment
|
||||||
|
image: golang:1.9.0-alpine3.6
|
||||||
|
before_script:
|
||||||
|
- apk add --no-cache git
|
||||||
|
- mkdir -p $GOPATH/src/$CI_PROJECT_NAMESPACE
|
||||||
|
- cp -r $(pwd) $GOPATH/src/$CI_PROJECT_PATH
|
||||||
|
- cd $GOPATH/src/$CI_PROJECT_PATH
|
||||||
|
- go get -u github.com/golang/dep/cmd/dep
|
||||||
|
- $GOPATH/bin/dep ensure
|
||||||
|
|
||||||
|
.docker_template: &setup_docker_environment
|
||||||
|
image: docker:latest
|
||||||
|
before_script:
|
||||||
|
- docker login -u gitlab-ci -p $CI_BUILD_TOKEN registry.ely.by
|
||||||
|
- export TEMP_IMAGE_NAME="$CONTAINER_IMAGE:$CI_PIPELINE_ID"
|
||||||
|
|
||||||
|
test:
|
||||||
|
<<: *setup_go_environment
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- ./script/coverage
|
||||||
|
|
||||||
|
build executable:
|
||||||
|
<<: *setup_go_environment
|
||||||
stage: build
|
stage: build
|
||||||
|
script:
|
||||||
|
- export VERSION="${CI_BUILD_TAG:-dev-$CI_BUILD_REF_NAME-${CI_BUILD_REF:0:8}+build-$CI_BUILD_ID}"
|
||||||
|
- >
|
||||||
|
env GOOS=linux
|
||||||
|
go build
|
||||||
|
-o $CI_PROJECT_DIR/minecraft-skinsystem
|
||||||
|
-ldflags "-X ${CI_PROJECT_NAMESPACE}/bootstrap.version=${VERSION}"
|
||||||
|
main.go
|
||||||
|
artifacts:
|
||||||
|
name: "${CI_JOB_STAGE} executable"
|
||||||
|
paths:
|
||||||
|
- $CI_PROJECT_DIR/minecraft-skinsystem
|
||||||
|
expire_in: 1 day
|
||||||
|
|
||||||
|
build docker image:
|
||||||
|
<<: *setup_docker_environment
|
||||||
|
stage: build_docker_image
|
||||||
|
script:
|
||||||
|
- docker build -t $TEMP_IMAGE_NAME -f docker/Dockerfile .
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
- develop
|
||||||
|
|
||||||
|
push dev:
|
||||||
|
<<: *setup_docker_environment
|
||||||
|
stage: push
|
||||||
|
variables:
|
||||||
|
GIT_STRATEGY: none
|
||||||
script:
|
script:
|
||||||
- export IMAGE_NAME="$CONTAINER_IMAGE:dev"
|
- export IMAGE_NAME="$CONTAINER_IMAGE:dev"
|
||||||
- docker build --pull -t $IMAGE_NAME .
|
- docker tag $TEMP_IMAGE_NAME $IMAGE_NAME
|
||||||
- docker push $IMAGE_NAME
|
- docker push $IMAGE_NAME
|
||||||
only:
|
only:
|
||||||
- develop
|
- develop
|
||||||
|
|
||||||
push_tags:
|
push tag:
|
||||||
|
<<: *setup_docker_environment
|
||||||
stage: push
|
stage: push
|
||||||
variables:
|
variables:
|
||||||
GIT_STRATEGY: none
|
GIT_STRATEGY: none
|
||||||
script:
|
script:
|
||||||
- export IMAGE_NAME="$CONTAINER_IMAGE:$CI_BUILD_TAG"
|
- export IMAGE_NAME="$CONTAINER_IMAGE:$CI_BUILD_TAG"
|
||||||
- docker tag $CONTAINER_IMAGE:dev $CONTAINER_IMAGE:latest
|
- export LATEST_IMAGE_NAME="$CONTAINER_IMAGE:latest"
|
||||||
- docker tag $CONTAINER_IMAGE:latest $IMAGE_NAME
|
- docker tag $TEMP_IMAGE_NAME $IMAGE_NAME
|
||||||
|
- docker tag $TEMP_IMAGE_NAME $LATEST_IMAGE_NAME
|
||||||
- docker push $IMAGE_NAME
|
- docker push $IMAGE_NAME
|
||||||
- docker push $CONTAINER_IMAGE:latest
|
- docker push $LATEST_IMAGE_NAME
|
||||||
only:
|
only:
|
||||||
- tags
|
- tags
|
||||||
|
|
||||||
|
cleanup temp image:
|
||||||
|
<<: *setup_docker_environment
|
||||||
|
stage: cleanup
|
||||||
|
when: always
|
||||||
|
script:
|
||||||
|
- docker rmi $TEMP_IMAGE_NAME || true
|
||||||
|
21
Dockerfile
21
Dockerfile
@ -1,21 +0,0 @@
|
|||||||
FROM golang:1.7-alpine
|
|
||||||
|
|
||||||
RUN apk add --no-cache git
|
|
||||||
|
|
||||||
RUN mkdir -p /go/src/elyby/minecraft-skinsystem \
|
|
||||||
/go/src/elyby/minecraft-skinsystem/data/capes \
|
|
||||||
&& ln -s /go/src/elyby/minecraft-skinsystem /go/src/app
|
|
||||||
|
|
||||||
WORKDIR /go/src/app
|
|
||||||
|
|
||||||
COPY ./minecraft-skinsystem.go /go/src/app/
|
|
||||||
COPY ./lib /go/src/app/lib
|
|
||||||
|
|
||||||
RUN go-wrapper download
|
|
||||||
RUN go-wrapper install
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
VOLUME ["/go/src/app"]
|
|
||||||
|
|
||||||
CMD ["go-wrapper", "run"]
|
|
189
Gopkg.lock
generated
Normal file
189
Gopkg.lock
generated
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||||
|
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/assembla/cony"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "dd62697b0adb9adfda8589520cb85f4cbc2361f1"
|
||||||
|
version = "v0.3.2"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/certifi/gocertifi"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "3fd9e1adb12b72d2f3f82191d49be9b93c69f67c"
|
||||||
|
version = "2017.07.27"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/davecgh/go-spew"
|
||||||
|
packages = ["spew"]
|
||||||
|
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||||
|
version = "v1.1.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/fsnotify/fsnotify"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "629574ca2a5df945712d3079857300b5e4da0236"
|
||||||
|
version = "v1.4.2"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/getsentry/raven-go"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "d175f85701dfbf44cb0510114c9943e665e60907"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/golang/mock"
|
||||||
|
packages = ["gomock"]
|
||||||
|
revision = "13f360950a79f5864a972c786a10a50e44b69541"
|
||||||
|
version = "v1.0.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/gorilla/context"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
|
||||||
|
version = "v1.1"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/gorilla/mux"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "bcd8bc72b08df0f70df986b97f95590779502d31"
|
||||||
|
version = "v1.4.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/hashicorp/hcl"
|
||||||
|
packages = [".","hcl/ast","hcl/parser","hcl/scanner","hcl/strconv","hcl/token","json/parser","json/scanner","json/token"]
|
||||||
|
revision = "8f6b1344a92ff8877cf24a5de9177bf7d0a2a187"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/inconshreveable/mousetrap"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
||||||
|
version = "v1.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/magiconair/properties"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "be5ece7dd465ab0765a9682137865547526d1dfb"
|
||||||
|
version = "v1.7.3"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/mediocregopher/radix.v2"
|
||||||
|
packages = ["cluster","pool","redis","util"]
|
||||||
|
revision = "d234cfb904a91daafa4e1f92599a893b349cc0c2"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/mitchellh/mapstructure"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "d0303fe809921458f417bcf828397a65db30a7e4"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/mono83/slf"
|
||||||
|
packages = [".","filters","params","rays","recievers","recievers/ansi","recievers/statsd","wd"]
|
||||||
|
revision = "8188a95c8d6b74c43953abb38b8bd6fdbc412ff5"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/mono83/udpwriter"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "a064bd7e3acfda563ea680b913b9ef24b7a73e15"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/pelletier/go-buffruneio"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "c37440a7cf42ac63b919c752ca73a85067e05992"
|
||||||
|
version = "v0.2.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/pelletier/go-toml"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "5ccdfb18c776b740aecaf085c4d9a2779199c279"
|
||||||
|
version = "v1.0.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/pmezard/go-difflib"
|
||||||
|
packages = ["difflib"]
|
||||||
|
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||||
|
version = "v1.0.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/spf13/afero"
|
||||||
|
packages = [".","mem"]
|
||||||
|
revision = "ee1bd8ee15a1306d1f9201acc41ef39cd9f99a1b"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/spf13/cast"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4"
|
||||||
|
version = "v1.1.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/spf13/cobra"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "3c0b56b677e04926dfa835a1b3f11cd4f62f076e"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/spf13/jwalterweatherman"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "12bd96e66386c1960ab0f74ced1362f66f552f7b"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/spf13/pflag"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
||||||
|
version = "v1.0.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/spf13/viper"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7"
|
||||||
|
version = "v1.0.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/streadway/amqp"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "2cbfe40c9341ad63ba23e53013b3ddc7989d801c"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/stretchr/testify"
|
||||||
|
packages = ["assert"]
|
||||||
|
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
|
||||||
|
version = "v1.1.4"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "golang.org/x/sys"
|
||||||
|
packages = ["unix"]
|
||||||
|
revision = "7ddbeae9ae08c6a06a59597f0c9edbc5ff2444ce"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "golang.org/x/text"
|
||||||
|
packages = ["internal/gen","internal/triegen","internal/ucd","transform","unicode/cldr","unicode/norm"]
|
||||||
|
revision = "bd91bbf73e9a4a801adbfb97133c992678533126"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "gopkg.in/h2non/gock.v1"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "84d599244901620fb3eb96473eb9e50619f69b47"
|
||||||
|
version = "v1.0.6"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "v2"
|
||||||
|
name = "gopkg.in/yaml.v2"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
||||||
|
|
||||||
|
[solve-meta]
|
||||||
|
analyzer-name = "dep"
|
||||||
|
analyzer-version = 1
|
||||||
|
inputs-digest = "dd545fafc23f9b6429b5b679ad5c213c14c819f1e4ea381823acf338651122e1"
|
||||||
|
solver-name = "gps-cdcl"
|
||||||
|
solver-version = 1
|
38
Gopkg.toml
Normal file
38
Gopkg.toml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
ignored = ["elyby/minecraft-skinsystem"]
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/gorilla/mux"
|
||||||
|
version = "1.4.0"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/mediocregopher/radix.v2"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/mono83/slf"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/spf13/cobra"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/spf13/viper"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/getsentry/raven-go"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/assembla/cony"
|
||||||
|
version = "^0.3.2"
|
||||||
|
|
||||||
|
# Testing dependencies
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/stretchr/testify"
|
||||||
|
version = "^1.1.4"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/golang/mock"
|
||||||
|
version = "^1.0.0"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "gopkg.in/h2non/gock.v1"
|
||||||
|
version = "^1.0.6"
|
82
README.md
82
README.md
@ -1,38 +1,74 @@
|
|||||||
# Это заготовка для нормального файла
|
# Ely.by Minecraft Skinsystem
|
||||||
|
|
||||||
Для настройки Dev-окружения нужно склонировать проект в удобное место,
|
Реализация API системы скинов для Minecraft v4.
|
||||||
за тем сделать символьную ссылку в свой GOPATH:
|
|
||||||
|
|
||||||
```sh
|
## Config
|
||||||
# Выполнять, находясь внутри директории репозитория
|
|
||||||
mkdir -p $GOPATH/src/elyby
|
|
||||||
ln -s $PWD $GOPATH/src/elyby/minecraft-skinsystem
|
|
||||||
```
|
|
||||||
|
|
||||||
Или можно склонировать репозиторий сразу в нужную локацию:
|
Конфигурация может задаваться посредством любого из перечисленных форматов файлов: JSON, TOML, YAML, HCL и
|
||||||
|
Java properties. Кроме того, параметры конфигурации могут перезаписываться доступными при запуске программы
|
||||||
|
ENV переменными.
|
||||||
|
|
||||||
```sh
|
> **Заметка**: ENV переменные именуются как KEY.SUBKEY.SUBSUBKEY, т.е. все символы должны быть заглавными,
|
||||||
git clone git@bitbucket.org:elyby/minecraft-skinsystem.git $GOPATH/src/elyby/minecraft-skinsystem
|
а точки должны отделять уровень вложенности.
|
||||||
```
|
|
||||||
|
|
||||||
Нужно скопировать правильный docker-compose файл для желаемого окружения:
|
Пример файла конфигурации находится в [config.dist.yml](config.dist.yml). Внутри dist-файла есть комментарии,
|
||||||
|
поясняющие назначение тех или иных параметров. Для работы его следует скопировать в локальный `config.yml`
|
||||||
|
и отредактировать под свои нужды.
|
||||||
|
|
||||||
```sh
|
## Развёртывание
|
||||||
cp docker-compose.dev.yml docker-compose.yml # dev env
|
|
||||||
cp docker-compose.prod.yml docker-compose.yml # prod env
|
|
||||||
```
|
|
||||||
|
|
||||||
И за тем всё это поднять:
|
Деплоить проект можно двумя способами:
|
||||||
|
|
||||||
|
1. Скомпилировав и запустив бинарный файл, а также обеспечив ему доступ ко всем необходмым сервисам.
|
||||||
|
|
||||||
|
2. Используя Docker и docker-compose.
|
||||||
|
|
||||||
|
*Первый случай не буду описывать, т.к. долго, мучительно и никто так делать не будет, я гарантирую это*,
|
||||||
|
поэтому перейдём сразу ко второму.
|
||||||
|
|
||||||
|
Прежде всего необходимо установить [Docker](https://docs.docker.com/engine/installation/) и
|
||||||
|
[docker-compose](https://docs.docker.com/compose/install/).
|
||||||
|
|
||||||
|
Для запуска последней версии проекта достаточно скопировать содержимое файла
|
||||||
|
[docker/docker-compose.prod.yml](docker/docker-compose.prod.yml) в файл `docker-compose.yml` непосредственно
|
||||||
|
на месте установки, после чего ввести в консоль команду:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Если нужно пересобрать весь контейнер, то выполняем следующее:
|
Web-приложение, amqp worker и все сопутствующие сервисы будут автоматически запущены. Данные из контейнеров
|
||||||
|
будут синхронизироваться в папку `data`.
|
||||||
|
|
||||||
|
## Разработка
|
||||||
|
|
||||||
|
Перво-наперво необходимо [установить последнюю версию Go](https://golang.org/doc/install) и сконфигурировать
|
||||||
|
переменную окружения GOPATH, а также установить инструмент контроля версий [dep](https://github.com/golang/dep).
|
||||||
|
|
||||||
|
Затем можно склонировать репозиторий хитрым способом, чтобы удовлетворить все прекрасные особенности Go:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Сперва создадим подпапку для приватных Go проектов Ely.by
|
||||||
|
mkdir -p $GOPATH/src/elyby
|
||||||
|
# Затем непосредственно клинируем репозиторий туда, где его ожидает увидеть Go
|
||||||
|
git clone git@gitlab.ely.by:elyby/minecraft-skinsystem.git $GOPATH/src/elyby/minecraft-skinsystem
|
||||||
|
# Переходим в папку проекта
|
||||||
|
cd $GOPATH/src/elyby/minecraft-skinsystem
|
||||||
|
# Устанавливаем зависимости
|
||||||
|
dep ensure
|
||||||
```
|
```
|
||||||
docker-compose stop app # Останавливаем конейтнер, если он ещё работает
|
|
||||||
docker-compose rm -f app # Удаляем конейтнер
|
Чтобы запустить проект достаточно написать `go run main.go`, но без файла конфигурации и Redis
|
||||||
docker-compose build app # Запускаем билд по новой
|
программа долго не проработает. Поэтому сперва копируем `config.dist.yml` в `config.yml` и, при необходимости,
|
||||||
docker-compose up -d app # Поднимаем свежесобранный контейнер обратно
|
затачиваем его под себя.
|
||||||
|
|
||||||
|
Redis можно установить в систему самостоятельно, но гораздо удобнее воспользоваться готовыми сервисами,
|
||||||
|
описанными в [docker/docker-compose.dev.yml](docker/docker-compose.dev.yml). Для этого просто копируем
|
||||||
|
`docker-compose.dev.yml` и поднимаем сервисы:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp docker/docker-compose.dev.yml docker-compose.yml
|
||||||
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
После этого `go run main.go serve` должен запустить web-сервер без дополнительной модификации файла конфигурации.
|
||||||
|
166
api/accounts/accounts.go
Normal file
166
api/accounts/accounts.go
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
package accounts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Addr string
|
||||||
|
Id string
|
||||||
|
Secret string
|
||||||
|
Scopes []string
|
||||||
|
|
||||||
|
Client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
config *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *Config) GetToken() (*Token, error) {
|
||||||
|
form := url.Values{}
|
||||||
|
form.Add("client_id", config.Id)
|
||||||
|
form.Add("client_secret", config.Secret)
|
||||||
|
form.Add("grant_type", "client_credentials")
|
||||||
|
form.Add("scope", strings.Join(config.Scopes, ","))
|
||||||
|
|
||||||
|
response, err := config.getHttpClient().Post(config.getTokenUrl(), "application/x-www-form-urlencoded", strings.NewReader(form.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
var result *Token
|
||||||
|
responseError := handleResponse(response)
|
||||||
|
if responseError != nil {
|
||||||
|
return nil, responseError
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
unmarshalError := json.Unmarshal(body, &result)
|
||||||
|
if unmarshalError != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.config = config
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *Config) getTokenUrl() string {
|
||||||
|
return concatenateHostAndPath(config.Addr, "/api/oauth2/v1/token")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *Config) getHttpClient() *http.Client {
|
||||||
|
if config.Client == nil {
|
||||||
|
config.Client = &http.Client{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccountInfoResponse struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (token *Token) AccountInfo(attribute string, value string) (*AccountInfoResponse, error) {
|
||||||
|
request := token.newRequest("GET", token.accountInfoUrl(), nil)
|
||||||
|
|
||||||
|
query := request.URL.Query()
|
||||||
|
query.Add(attribute, value)
|
||||||
|
request.URL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
response, err := token.config.Client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
var info *AccountInfoResponse
|
||||||
|
|
||||||
|
responseError := handleResponse(response)
|
||||||
|
if responseError != nil {
|
||||||
|
return nil, responseError
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
json.Unmarshal(body, &info)
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (token *Token) accountInfoUrl() string {
|
||||||
|
return concatenateHostAndPath(token.config.Addr, "/api/internal/accounts/info")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (token *Token) newRequest(method string, urlStr string, body io.Reader) *http.Request {
|
||||||
|
request, err := http.NewRequest(method, urlStr, body)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Add("Authorization", "Bearer " + token.AccessToken)
|
||||||
|
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
func concatenateHostAndPath(host string, pathToJoin string) string {
|
||||||
|
u, _ := url.Parse(host)
|
||||||
|
u.Path = path.Join(u.Path, pathToJoin)
|
||||||
|
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnauthorizedResponse struct {}
|
||||||
|
|
||||||
|
func (err UnauthorizedResponse) Error() string {
|
||||||
|
return "Unauthorized response"
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForbiddenResponse struct {}
|
||||||
|
|
||||||
|
func (err ForbiddenResponse) Error() string {
|
||||||
|
return "Forbidden response"
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotFoundResponse struct {}
|
||||||
|
|
||||||
|
func (err NotFoundResponse) Error() string {
|
||||||
|
return "Not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotSuccessResponse struct {
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err NotSuccessResponse) Error() string {
|
||||||
|
return fmt.Sprintf("Response code is \"%d\"", err.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleResponse(response *http.Response) error {
|
||||||
|
switch status := response.StatusCode; status {
|
||||||
|
case 200:
|
||||||
|
return nil
|
||||||
|
case 401:
|
||||||
|
return &UnauthorizedResponse{}
|
||||||
|
case 403:
|
||||||
|
return &ForbiddenResponse{}
|
||||||
|
case 404:
|
||||||
|
return &NotFoundResponse{}
|
||||||
|
default:
|
||||||
|
return &NotSuccessResponse{status}
|
||||||
|
}
|
||||||
|
}
|
98
api/accounts/accounts_test.go
Normal file
98
api/accounts/accounts_test.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package accounts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
"gopkg.in/h2non/gock.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_GetToken(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
defer gock.Off()
|
||||||
|
gock.New("https://account.ely.by").
|
||||||
|
Post("/api/oauth2/v1/token").
|
||||||
|
Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")).
|
||||||
|
Reply(200).
|
||||||
|
JSON(map[string]interface{}{
|
||||||
|
"access_token": "mocked-token",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 86400,
|
||||||
|
})
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
gock.InterceptClient(client)
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
Addr: "https://account.ely.by",
|
||||||
|
Id: "mock-id",
|
||||||
|
Secret: "mock-secret",
|
||||||
|
Scopes: []string{"scope1", "scope2"},
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := config.GetToken()
|
||||||
|
if assert.NoError(err) {
|
||||||
|
assert.Equal("mocked-token", result.AccessToken)
|
||||||
|
assert.Equal("Bearer", result.TokenType)
|
||||||
|
assert.Equal(86400, result.ExpiresIn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToken_AccountInfo(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
defer gock.Off()
|
||||||
|
// To test valid behavior
|
||||||
|
gock.New("https://account.ely.by").
|
||||||
|
Get("/api/internal/accounts/info").
|
||||||
|
MatchParam("id", "1").
|
||||||
|
MatchHeader("Authorization", "Bearer mock-token").
|
||||||
|
Reply(200).
|
||||||
|
JSON(map[string]interface{}{
|
||||||
|
"id": 1,
|
||||||
|
"uuid": "0f657aa8-bfbe-415d-b700-5750090d3af3",
|
||||||
|
"username": "dummy",
|
||||||
|
"email": "dummy@ely.by",
|
||||||
|
})
|
||||||
|
|
||||||
|
// To test behavior on invalid or expired token
|
||||||
|
gock.New("https://account.ely.by").
|
||||||
|
Get("/api/internal/accounts/info").
|
||||||
|
MatchParam("id", "1").
|
||||||
|
MatchHeader("Authorization", "Bearer mock-token").
|
||||||
|
Reply(401).
|
||||||
|
JSON(map[string]interface{}{
|
||||||
|
"name": "Unauthorized",
|
||||||
|
"message": "Incorrect token",
|
||||||
|
"code": 0,
|
||||||
|
"status": 401,
|
||||||
|
})
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
gock.InterceptClient(client)
|
||||||
|
|
||||||
|
token := &Token{
|
||||||
|
AccessToken: "mock-token",
|
||||||
|
config: &Config{
|
||||||
|
Addr: "https://account.ely.by",
|
||||||
|
Client: client,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := token.AccountInfo("id", "1")
|
||||||
|
if assert.NoError(err) {
|
||||||
|
assert.Equal(1, result.Id)
|
||||||
|
assert.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", result.Uuid)
|
||||||
|
assert.Equal("dummy", result.Username)
|
||||||
|
assert.Equal("dummy@ely.by", result.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
result2, err2 := token.AccountInfo("id", "1")
|
||||||
|
assert.Nil(result2)
|
||||||
|
assert.Error(err2)
|
||||||
|
assert.IsType(&UnauthorizedResponse{}, err2)
|
||||||
|
}
|
56
api/accounts/auto-refresh-token.go
Normal file
56
api/accounts/auto-refresh-token.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package accounts
|
||||||
|
|
||||||
|
type AutoRefresh struct {
|
||||||
|
token *Token
|
||||||
|
config *Config
|
||||||
|
repeatsCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
const repeatsLimit = 3
|
||||||
|
|
||||||
|
func (config *Config) GetTokenWithAutoRefresh() *AutoRefresh {
|
||||||
|
return &AutoRefresh{
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (refresher *AutoRefresh) AccountInfo(attribute string, value string) (*AccountInfoResponse, error) {
|
||||||
|
defer refresher.resetRepeatsCount()
|
||||||
|
|
||||||
|
apiToken, err := refresher.getToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := apiToken.AccountInfo(attribute, value)
|
||||||
|
if err != nil {
|
||||||
|
_, isTokenExpire := err.(*UnauthorizedResponse)
|
||||||
|
if !isTokenExpire || refresher.repeatsCount >= repeatsLimit - 1 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
refresher.repeatsCount++
|
||||||
|
refresher.token = nil
|
||||||
|
|
||||||
|
return refresher.AccountInfo(attribute, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (refresher *AutoRefresh) getToken() (*Token, error) {
|
||||||
|
if refresher.token == nil {
|
||||||
|
newToken, err := refresher.config.GetToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
refresher.token = newToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return refresher.token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (refresher *AutoRefresh) resetRepeatsCount() {
|
||||||
|
refresher.repeatsCount = 0
|
||||||
|
}
|
242
api/accounts/auto-refresh-token_test.go
Normal file
242
api/accounts/auto-refresh-token_test.go
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
package accounts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
"gopkg.in/h2non/gock.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var config = &Config{
|
||||||
|
Addr: "https://account.ely.by",
|
||||||
|
Id: "mock-id",
|
||||||
|
Secret: "mock-secret",
|
||||||
|
Scopes: []string{"scope1", "scope2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_GetTokenWithAutoRefresh(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
testConfig := &Config{}
|
||||||
|
*testConfig = *config
|
||||||
|
|
||||||
|
result := testConfig.GetTokenWithAutoRefresh()
|
||||||
|
assert.Equal(testConfig, result.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoRefresh_AccountInfo(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
defer gock.Off()
|
||||||
|
gock.New("https://account.ely.by").
|
||||||
|
Post("/api/oauth2/v1/token").
|
||||||
|
Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")).
|
||||||
|
Reply(200).
|
||||||
|
JSON(map[string]interface{}{
|
||||||
|
"access_token": "mocked-token",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 86400,
|
||||||
|
})
|
||||||
|
|
||||||
|
gock.New("https://account.ely.by").
|
||||||
|
Get("/api/internal/accounts/info").
|
||||||
|
Times(2).
|
||||||
|
MatchParam("id", "1").
|
||||||
|
MatchHeader("Authorization", "Bearer mocked-token").
|
||||||
|
Reply(200).
|
||||||
|
JSON(map[string]interface{}{
|
||||||
|
"id": 1,
|
||||||
|
"uuid": "0f657aa8-bfbe-415d-b700-5750090d3af3",
|
||||||
|
"username": "dummy",
|
||||||
|
"email": "dummy@ely.by",
|
||||||
|
})
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
gock.InterceptClient(client)
|
||||||
|
|
||||||
|
testConfig := &Config{}
|
||||||
|
*testConfig = *config
|
||||||
|
testConfig.Client = client
|
||||||
|
|
||||||
|
autoRefresher := testConfig.GetTokenWithAutoRefresh()
|
||||||
|
result, err := autoRefresher.AccountInfo("id", "1")
|
||||||
|
if assert.NoError(err) {
|
||||||
|
assert.Equal(1, result.Id)
|
||||||
|
assert.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", result.Uuid)
|
||||||
|
assert.Equal("dummy", result.Username)
|
||||||
|
assert.Equal("dummy@ely.by", result.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
result2, err2 := autoRefresher.AccountInfo("id", "1")
|
||||||
|
if assert.NoError(err2) {
|
||||||
|
assert.Equal(result, result2, "Results should still be same without token refreshing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoRefresh_AccountInfo2(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
defer gock.Off()
|
||||||
|
gock.New("https://account.ely.by").
|
||||||
|
Post("/api/oauth2/v1/token").
|
||||||
|
Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")).
|
||||||
|
Reply(200).
|
||||||
|
JSON(map[string]interface{}{
|
||||||
|
"access_token": "mocked-token-1",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 86400,
|
||||||
|
})
|
||||||
|
|
||||||
|
gock.New("https://account.ely.by").
|
||||||
|
Get("/api/internal/accounts/info").
|
||||||
|
MatchParam("id", "1").
|
||||||
|
MatchHeader("Authorization", "Bearer mocked-token-1").
|
||||||
|
Reply(200).
|
||||||
|
JSON(map[string]interface{}{
|
||||||
|
"id": 1,
|
||||||
|
"uuid": "0f657aa8-bfbe-415d-b700-5750090d3af3",
|
||||||
|
"username": "dummy",
|
||||||
|
"email": "dummy@ely.by",
|
||||||
|
})
|
||||||
|
|
||||||
|
gock.New("https://account.ely.by").
|
||||||
|
Get("/api/internal/accounts/info").
|
||||||
|
MatchParam("id", "1").
|
||||||
|
MatchHeader("Authorization", "Bearer mocked-token-1").
|
||||||
|
Reply(401).
|
||||||
|
JSON(map[string]interface{}{
|
||||||
|
"name": "Unauthorized",
|
||||||
|
"message": "Incorrect token",
|
||||||
|
"code": 0,
|
||||||
|
"status": 401,
|
||||||
|
})
|
||||||
|
|
||||||
|
gock.New("https://account.ely.by").
|
||||||
|
Post("/api/oauth2/v1/token").
|
||||||
|
Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")).
|
||||||
|
Reply(200).
|
||||||
|
JSON(map[string]interface{}{
|
||||||
|
"access_token": "mocked-token-2",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 86400,
|
||||||
|
})
|
||||||
|
|
||||||
|
gock.New("https://account.ely.by").
|
||||||
|
Get("/api/internal/accounts/info").
|
||||||
|
MatchParam("id", "1").
|
||||||
|
MatchHeader("Authorization", "Bearer mocked-token-2").
|
||||||
|
Reply(200).
|
||||||
|
JSON(map[string]interface{}{
|
||||||
|
"id": 1,
|
||||||
|
"uuid": "0f657aa8-bfbe-415d-b700-5750090d3af3",
|
||||||
|
"username": "dummy",
|
||||||
|
"email": "dummy@ely.by",
|
||||||
|
})
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
gock.InterceptClient(client)
|
||||||
|
|
||||||
|
testConfig := &Config{}
|
||||||
|
*testConfig = *config
|
||||||
|
testConfig.Client = client
|
||||||
|
|
||||||
|
autoRefresher := testConfig.GetTokenWithAutoRefresh()
|
||||||
|
result, err := autoRefresher.AccountInfo("id", "1")
|
||||||
|
if assert.NoError(err) {
|
||||||
|
assert.Equal(1, result.Id)
|
||||||
|
assert.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", result.Uuid)
|
||||||
|
assert.Equal("dummy", result.Username)
|
||||||
|
assert.Equal("dummy@ely.by", result.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
result2, err2 := autoRefresher.AccountInfo("id", "1")
|
||||||
|
if assert.NoError(err2) {
|
||||||
|
assert.Equal(result, result2, "Results should still be same with refreshed token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoRefresh_AccountInfo3(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
defer gock.Off()
|
||||||
|
gock.New("https://account.ely.by").
|
||||||
|
Post("/api/oauth2/v1/token").
|
||||||
|
Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")).
|
||||||
|
Reply(200).
|
||||||
|
JSON(map[string]interface{}{
|
||||||
|
"access_token": "mocked-token-1",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 86400,
|
||||||
|
})
|
||||||
|
|
||||||
|
gock.New("https://account.ely.by").
|
||||||
|
Get("/api/internal/accounts/info").
|
||||||
|
MatchParam("id", "1").
|
||||||
|
MatchHeader("Authorization", "Bearer mocked-token-1").
|
||||||
|
Reply(404).
|
||||||
|
JSON(map[string]interface{}{
|
||||||
|
"name": "Not Found",
|
||||||
|
"message": "Page not found.",
|
||||||
|
"code": 0,
|
||||||
|
"status": 404,
|
||||||
|
})
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
gock.InterceptClient(client)
|
||||||
|
|
||||||
|
testConfig := &Config{}
|
||||||
|
*testConfig = *config
|
||||||
|
testConfig.Client = client
|
||||||
|
|
||||||
|
autoRefresher := testConfig.GetTokenWithAutoRefresh()
|
||||||
|
result, err := autoRefresher.AccountInfo("id", "1")
|
||||||
|
assert.Nil(result)
|
||||||
|
assert.Error(err)
|
||||||
|
assert.IsType(&NotFoundResponse{}, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoRefresh_AccountInfo4(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
defer gock.Off()
|
||||||
|
gock.New("https://account.ely.by").
|
||||||
|
Post("/api/oauth2/v1/token").
|
||||||
|
Times(3).
|
||||||
|
Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")).
|
||||||
|
Reply(200).
|
||||||
|
JSON(map[string]interface{}{
|
||||||
|
"access_token": "mocked-token-1",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 86400,
|
||||||
|
})
|
||||||
|
|
||||||
|
gock.New("https://account.ely.by").
|
||||||
|
Get("/api/internal/accounts/info").
|
||||||
|
Times(3).
|
||||||
|
MatchParam("id", "1").
|
||||||
|
MatchHeader("Authorization", "Bearer mocked-token-1").
|
||||||
|
Reply(401).
|
||||||
|
JSON(map[string]interface{}{
|
||||||
|
"name": "Unauthorized",
|
||||||
|
"message": "Incorrect token",
|
||||||
|
"code": 0,
|
||||||
|
"status": 401,
|
||||||
|
})
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
gock.InterceptClient(client)
|
||||||
|
|
||||||
|
testConfig := &Config{}
|
||||||
|
*testConfig = *config
|
||||||
|
testConfig.Client = client
|
||||||
|
|
||||||
|
autoRefresher := testConfig.GetTokenWithAutoRefresh()
|
||||||
|
result, err := autoRefresher.AccountInfo("id", "1")
|
||||||
|
assert.Nil(result)
|
||||||
|
assert.Error(err)
|
||||||
|
if !assert.IsType(&UnauthorizedResponse{}, err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
91
bootstrap/bootstrap.go
Normal file
91
bootstrap/bootstrap.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/assembla/cony"
|
||||||
|
"github.com/getsentry/raven-go"
|
||||||
|
"github.com/mono83/slf/rays"
|
||||||
|
"github.com/mono83/slf/recievers/statsd"
|
||||||
|
"github.com/mono83/slf/recievers/writer"
|
||||||
|
"github.com/mono83/slf/wd"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/logger/receivers/sentry"
|
||||||
|
)
|
||||||
|
|
||||||
|
var version = ""
|
||||||
|
|
||||||
|
func GetVersion() string {
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateLogger(statsdAddr string, sentryAddr string) (wd.Watchdog, error) {
|
||||||
|
wd.AddReceiver(writer.New(writer.Options{
|
||||||
|
Marker: false,
|
||||||
|
TimeFormat: "15:04:05.000",
|
||||||
|
}))
|
||||||
|
if statsdAddr != "" {
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
statsdReceiver, err := statsd.NewReceiver(statsd.Config{
|
||||||
|
Address: statsdAddr,
|
||||||
|
Prefix: "ely.skinsystem." + hostname + ".app.",
|
||||||
|
FlushEvery: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
wd.AddReceiver(statsdReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sentryAddr != "" {
|
||||||
|
ravenClient, err := raven.New(sentryAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ravenClient.SetEnvironment("production")
|
||||||
|
ravenClient.SetDefaultLoggerName("sentry-watchdog-receiver")
|
||||||
|
programVersion := GetVersion()
|
||||||
|
if programVersion != "" {
|
||||||
|
raven.SetRelease(programVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
sentryReceiver, err := sentry.NewReceiverWithCustomRaven(ravenClient, &sentry.Config{
|
||||||
|
MinLevel: "warn",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
wd.AddReceiver(sentryReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
return wd.New("", "").WithParams(rays.Host), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RabbitMQConfig struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
Vhost string
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateRabbitMQClient(config *RabbitMQConfig) *cony.Client {
|
||||||
|
addr := fmt.Sprintf(
|
||||||
|
"amqp://%s:%s@%s:%d/%s",
|
||||||
|
config.Username,
|
||||||
|
config.Password,
|
||||||
|
config.Host,
|
||||||
|
config.Port,
|
||||||
|
url.PathEscape(config.Vhost),
|
||||||
|
)
|
||||||
|
|
||||||
|
client := cony.NewClient(cony.URL(addr), cony.Backoff(cony.DefaultBackoff))
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
67
cmd/amqpWorker.go
Normal file
67
cmd/amqpWorker.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/api/accounts"
|
||||||
|
"elyby/minecraft-skinsystem/bootstrap"
|
||||||
|
"elyby/minecraft-skinsystem/db"
|
||||||
|
"elyby/minecraft-skinsystem/worker"
|
||||||
|
)
|
||||||
|
|
||||||
|
var amqpWorkerCmd = &cobra.Command{
|
||||||
|
Use: "amqp-worker",
|
||||||
|
Short: "Launches a worker which listens to events and processes them",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
logger, err := bootstrap.CreateLogger(viper.GetString("statsd.addr"), viper.GetString("sentry.dsn"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(fmt.Printf("Cannot initialize logger: %v", err))
|
||||||
|
}
|
||||||
|
logger.Info("Logger successfully initialized")
|
||||||
|
|
||||||
|
storageFactory := db.StorageFactory{Config: viper.GetViper()}
|
||||||
|
|
||||||
|
logger.Info("Initializing skins repository")
|
||||||
|
skinsRepo, err := storageFactory.CreateFactory("redis").CreateSkinsRepository()
|
||||||
|
if err != nil {
|
||||||
|
logger.Emergency(fmt.Sprintf("Error on creating skins repo: %+v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("Skins repository successfully initialized")
|
||||||
|
|
||||||
|
logger.Info("Creating AMQP client")
|
||||||
|
amqpClient := bootstrap.CreateRabbitMQClient(&bootstrap.RabbitMQConfig{
|
||||||
|
Host: viper.GetString("amqp.host"),
|
||||||
|
Port: viper.GetInt("amqp.port"),
|
||||||
|
Username: viper.GetString("amqp.username"),
|
||||||
|
Password: viper.GetString("amqp.password"),
|
||||||
|
Vhost: viper.GetString("amqp.vhost"),
|
||||||
|
})
|
||||||
|
|
||||||
|
accountsApi := (&accounts.Config{
|
||||||
|
Addr: viper.GetString("api.accounts.host"),
|
||||||
|
Id: viper.GetString("api.accounts.id"),
|
||||||
|
Secret: viper.GetString("api.accounts.secret"),
|
||||||
|
Scopes: viper.GetStringSlice("api.accounts.scopes"),
|
||||||
|
}).GetTokenWithAutoRefresh()
|
||||||
|
|
||||||
|
services := &worker.Services{
|
||||||
|
Logger: logger,
|
||||||
|
AmqpClient: amqpClient,
|
||||||
|
SkinsRepo: skinsRepo,
|
||||||
|
AccountsAPI: accountsApi,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := services.Run(); err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("Cannot initialize worker: %+v", err))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(amqpWorkerCmd)
|
||||||
|
}
|
46
cmd/root.go
Normal file
46
cmd/root.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cfgFile string
|
||||||
|
|
||||||
|
var RootCmd = &cobra.Command{
|
||||||
|
Use: "",
|
||||||
|
Short: "Nothing here",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
|
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||||
|
func Execute() {
|
||||||
|
if err := RootCmd.Execute(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cobra.OnInitialize(initConfig)
|
||||||
|
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.test.yaml)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func initConfig() {
|
||||||
|
if cfgFile != "" {
|
||||||
|
viper.SetConfigFile(cfgFile)
|
||||||
|
} else {
|
||||||
|
viper.SetConfigName("config")
|
||||||
|
viper.AddConfigPath("/etc/minecraft-skinsystem")
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.AutomaticEnv()
|
||||||
|
|
||||||
|
if err := viper.ReadInConfig(); err == nil {
|
||||||
|
fmt.Println("Using config file:", viper.ConfigFileUsed())
|
||||||
|
}
|
||||||
|
}
|
58
cmd/serve.go
Normal file
58
cmd/serve.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/bootstrap"
|
||||||
|
"elyby/minecraft-skinsystem/db"
|
||||||
|
"elyby/minecraft-skinsystem/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var serveCmd = &cobra.Command{
|
||||||
|
Use: "serve",
|
||||||
|
Short: "Runs the system server skins",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
logger, err := bootstrap.CreateLogger(viper.GetString("statsd.addr"), viper.GetString("sentry.dsn"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(fmt.Printf("Cannot initialize logger: %v", err))
|
||||||
|
}
|
||||||
|
logger.Info("Logger successfully initialized")
|
||||||
|
|
||||||
|
storageFactory := db.StorageFactory{Config: viper.GetViper()}
|
||||||
|
|
||||||
|
logger.Info("Initializing skins repository")
|
||||||
|
skinsRepo, err := storageFactory.CreateFactory("redis").CreateSkinsRepository()
|
||||||
|
if err != nil {
|
||||||
|
logger.Emergency(fmt.Sprintf("Error on creating skins repo: %+v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("Skins repository successfully initialized")
|
||||||
|
|
||||||
|
logger.Info("Initializing capes repository")
|
||||||
|
capesRepo, err := storageFactory.CreateFactory("filesystem").CreateCapesRepository()
|
||||||
|
if err != nil {
|
||||||
|
logger.Emergency(fmt.Sprintf("Error on creating capes repo: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("Capes repository successfully initialized")
|
||||||
|
|
||||||
|
cfg := &http.Config{
|
||||||
|
ListenSpec: fmt.Sprintf("%s:%d", viper.GetString("server.host"), viper.GetInt("server.port")),
|
||||||
|
SkinsRepo: skinsRepo,
|
||||||
|
CapesRepo: capesRepo,
|
||||||
|
Logger: logger,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfg.Run(); err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("Error in main(): %v", err))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(serveCmd)
|
||||||
|
}
|
51
config.dist.yml
Normal file
51
config.dist.yml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Main server configuration. Actually you don't want to change it,
|
||||||
|
# but you able to change host or port, that will be used by serve command
|
||||||
|
server:
|
||||||
|
host: localhost
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
# Worker listen to AMQP events, so it should know how to connect to any
|
||||||
|
# AMQP provider (actually RabbitMQ). You should not escape any vhost
|
||||||
|
# characters, 'cause it will be done by application automatically
|
||||||
|
amqp:
|
||||||
|
host: localhost
|
||||||
|
port: 5672
|
||||||
|
username: amqp-user
|
||||||
|
password: amqp-password
|
||||||
|
vhost: /
|
||||||
|
|
||||||
|
# Both of web or worker depends on storage.
|
||||||
|
storage:
|
||||||
|
# For now app require Redis and don't support any other backends to store
|
||||||
|
# skins, but in the future we can have more backends. Poll size tune amount
|
||||||
|
# of connections to the redis. It's not recommended to set it less then 2
|
||||||
|
# because it will lead to panic on high load.
|
||||||
|
redis:
|
||||||
|
host: localhost
|
||||||
|
port: 6379
|
||||||
|
poolSize: 10
|
||||||
|
|
||||||
|
# Filesystem storage used to store capes. basePath specify absolute or relative
|
||||||
|
# path to storage and capesDirName specify which folder in this base path will
|
||||||
|
# be used to search capes.
|
||||||
|
filesystem:
|
||||||
|
basePath: data
|
||||||
|
capesDirName: capes
|
||||||
|
|
||||||
|
# Accounts Ely.by internal API will be used in cases, when by some reasons
|
||||||
|
# information about user will be unavailable in the app storage.
|
||||||
|
api:
|
||||||
|
accounts:
|
||||||
|
host: https://account.ely.by
|
||||||
|
id: app-id
|
||||||
|
secret: secret
|
||||||
|
scopes:
|
||||||
|
- internal_account_info
|
||||||
|
|
||||||
|
# StatsD can be used to collect metrics
|
||||||
|
# statsd:
|
||||||
|
# addr: localhost:3746
|
||||||
|
|
||||||
|
# Sentry can be used to collect app errors
|
||||||
|
# sentry:
|
||||||
|
# dsn: "https://public:private@your.sentry.io/1"
|
2
data/statsd/.gitignore
vendored
2
data/statsd/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
*
|
|
||||||
!.gitignore
|
|
25
db/commons.go
Normal file
25
db/commons.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
type ParamRequired struct {
|
||||||
|
Param string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ParamRequired) Error() string {
|
||||||
|
return "Required parameter not provided"
|
||||||
|
}
|
||||||
|
|
||||||
|
type SkinNotFoundError struct {
|
||||||
|
Who string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e SkinNotFoundError) Error() string {
|
||||||
|
return "Skin data not found."
|
||||||
|
}
|
||||||
|
|
||||||
|
type CapeNotFoundError struct {
|
||||||
|
Who string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e CapeNotFoundError) Error() string {
|
||||||
|
return "Cape file not found."
|
||||||
|
}
|
34
db/factory.go
Normal file
34
db/factory.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/interfaces"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StorageFactory struct {
|
||||||
|
Config *viper.Viper
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepositoriesCreator interface {
|
||||||
|
CreateSkinsRepository() (interfaces.SkinsRepository, error)
|
||||||
|
CreateCapesRepository() (interfaces.CapesRepository, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (factory *StorageFactory) CreateFactory(backend string) RepositoriesCreator {
|
||||||
|
switch backend {
|
||||||
|
case "redis":
|
||||||
|
return &RedisFactory{
|
||||||
|
Host: factory.Config.GetString("storage.redis.host"),
|
||||||
|
Port: factory.Config.GetInt("storage.redis.port"),
|
||||||
|
PoolSize: factory.Config.GetInt("storage.redis.poolSize"),
|
||||||
|
}
|
||||||
|
case "filesystem":
|
||||||
|
return &FilesystemFactory{
|
||||||
|
BasePath : factory.Config.GetString("storage.filesystem.basePath"),
|
||||||
|
CapesDirName: factory.Config.GetString("storage.filesystem.capesDirName"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
59
db/filesystem.go
Normal file
59
db/filesystem.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/interfaces"
|
||||||
|
"elyby/minecraft-skinsystem/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FilesystemFactory struct {
|
||||||
|
BasePath string
|
||||||
|
CapesDirName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FilesystemFactory) CreateSkinsRepository() (interfaces.SkinsRepository, error) {
|
||||||
|
panic("skins repository not supported for this storage type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FilesystemFactory) CreateCapesRepository() (interfaces.CapesRepository, error) {
|
||||||
|
if err := f.validateFactoryConfig(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &filesStorage{path: path.Join(f.BasePath, f.CapesDirName)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FilesystemFactory) validateFactoryConfig() error {
|
||||||
|
if f.BasePath == "" {
|
||||||
|
return &ParamRequired{"basePath"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.CapesDirName == "" {
|
||||||
|
f.CapesDirName = "capes"
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type filesStorage struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repository *filesStorage) FindByUsername(username string) (*model.Cape, error) {
|
||||||
|
if username == "" {
|
||||||
|
return nil, &CapeNotFoundError{username}
|
||||||
|
}
|
||||||
|
|
||||||
|
capePath := path.Join(repository.path, strings.ToLower(username) + ".png")
|
||||||
|
file, err := os.Open(capePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &CapeNotFoundError{username}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Cape{
|
||||||
|
File: file,
|
||||||
|
}, nil
|
||||||
|
}
|
198
db/redis.go
Normal file
198
db/redis.go
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/zlib"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/radix.v2/pool"
|
||||||
|
"github.com/mediocregopher/radix.v2/redis"
|
||||||
|
"github.com/mediocregopher/radix.v2/util"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/interfaces"
|
||||||
|
"elyby/minecraft-skinsystem/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RedisFactory struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
PoolSize int
|
||||||
|
connection util.Cmder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f RedisFactory) CreateSkinsRepository() (interfaces.SkinsRepository, error) {
|
||||||
|
connection, err := f.getConnection()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &redisDb{connection}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f RedisFactory) CreateCapesRepository() (interfaces.CapesRepository, error) {
|
||||||
|
panic("capes repository not supported for this storage type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f RedisFactory) getConnection() (util.Cmder, error) {
|
||||||
|
if f.connection == nil {
|
||||||
|
if f.Host == "" {
|
||||||
|
return nil, &ParamRequired{"host"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.Port == 0 {
|
||||||
|
return nil, &ParamRequired{"port"}
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("%s:%d", f.Host, f.Port)
|
||||||
|
conn, err := createConnection(addr, f.PoolSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.connection = conn
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
period := 5
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Duration(period) * time.Second)
|
||||||
|
resp := f.connection.Cmd("PING")
|
||||||
|
if resp.Err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Redis not pinged. Try to reconnect")
|
||||||
|
conn, err := createConnection(addr, f.PoolSize)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Cannot reconnect to redis: %v\n", err)
|
||||||
|
log.Printf("Waiting %d seconds to retry\n", period)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
f.connection = conn
|
||||||
|
log.Println("Reconnected")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.connection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createConnection(addr string, poolSize int) (util.Cmder, error) {
|
||||||
|
if poolSize > 1 {
|
||||||
|
return pool.New("tcp", addr, poolSize)
|
||||||
|
} else {
|
||||||
|
return redis.Dial("tcp", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type redisDb struct {
|
||||||
|
conn util.Cmder
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountIdToUsernameKey string = "hash:username-to-account-id"
|
||||||
|
|
||||||
|
func (db *redisDb) FindByUsername(username string) (*model.Skin, error) {
|
||||||
|
if username == "" {
|
||||||
|
return nil, &SkinNotFoundError{username}
|
||||||
|
}
|
||||||
|
|
||||||
|
redisKey := buildKey(username)
|
||||||
|
response := db.conn.Cmd("GET", redisKey)
|
||||||
|
if response.IsType(redis.Nil) {
|
||||||
|
return nil, &SkinNotFoundError{username}
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedResult, err := response.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := zlibDecode(encodedResult)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Cannot uncompress zlib for key " + redisKey) // TODO: replace with valid error
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var skin *model.Skin
|
||||||
|
err = json.Unmarshal(result, &skin)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Cannot decode record data for key" + redisKey) // TODO: replace with valid error
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
skin.OldUsername = skin.Username
|
||||||
|
|
||||||
|
return skin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *redisDb) FindByUserId(id int) (*model.Skin, error) {
|
||||||
|
response := db.conn.Cmd("HGET", accountIdToUsernameKey, id)
|
||||||
|
if response.IsType(redis.Nil) {
|
||||||
|
return nil, SkinNotFoundError{"unknown"}
|
||||||
|
}
|
||||||
|
|
||||||
|
username, _ := response.Str()
|
||||||
|
|
||||||
|
return db.FindByUsername(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *redisDb) Save(skin *model.Skin) error {
|
||||||
|
conn := db.conn
|
||||||
|
if poolConn, isPool := conn.(*pool.Pool); isPool {
|
||||||
|
conn, _ = poolConn.Get()
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.Cmd("MULTI")
|
||||||
|
|
||||||
|
// Если пользователь сменил ник, то мы должны удать его ключ
|
||||||
|
if skin.OldUsername != "" && skin.OldUsername != skin.Username {
|
||||||
|
conn.Cmd("DEL", buildKey(skin.OldUsername))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если это новая запись или если пользователь сменил ник, то обновляем значение в хэш-таблице
|
||||||
|
if skin.OldUsername != "" || skin.OldUsername != skin.Username {
|
||||||
|
conn.Cmd("HSET", accountIdToUsernameKey, skin.UserId, skin.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
str, _ := json.Marshal(skin)
|
||||||
|
conn.Cmd("SET", buildKey(skin.Username), zlibEncode(str))
|
||||||
|
|
||||||
|
conn.Cmd("EXEC")
|
||||||
|
|
||||||
|
skin.OldUsername = skin.Username
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildKey(username string) string {
|
||||||
|
return "username:" + strings.ToLower(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
//noinspection GoUnusedFunction
|
||||||
|
func zlibEncode(str []byte) []byte {
|
||||||
|
var buff bytes.Buffer
|
||||||
|
writer := zlib.NewWriter(&buff)
|
||||||
|
writer.Write(str)
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
return buff.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func zlibDecode(bts []byte) ([]byte, error) {
|
||||||
|
buff := bytes.NewReader(bts)
|
||||||
|
reader, readError := zlib.NewReader(buff)
|
||||||
|
if readError != nil {
|
||||||
|
return nil, readError
|
||||||
|
}
|
||||||
|
|
||||||
|
resultBuffer := new(bytes.Buffer)
|
||||||
|
io.Copy(resultBuffer, reader)
|
||||||
|
reader.Close()
|
||||||
|
|
||||||
|
return resultBuffer.Bytes(), nil
|
||||||
|
}
|
@ -1,20 +0,0 @@
|
|||||||
version: '2'
|
|
||||||
services:
|
|
||||||
redis:
|
|
||||||
image: redis:3.2-32bit
|
|
||||||
volumes:
|
|
||||||
- ./data/redis:/data
|
|
||||||
|
|
||||||
rabbitmq:
|
|
||||||
image: rabbitmq:3.6
|
|
||||||
environment:
|
|
||||||
RABBITMQ_DEFAULT_USER: "ely-skinsystem-app"
|
|
||||||
RABBITMQ_DEFAULT_PASS: "ely-skinsystem-app-password"
|
|
||||||
RABBITMQ_DEFAULT_VHOST: "/ely"
|
|
||||||
|
|
||||||
statsd:
|
|
||||||
image: hopsoft/graphite-statsd
|
|
||||||
volumes:
|
|
||||||
- ./data/statsd:/opt/graphite/storage
|
|
||||||
- ./data/graphite-config:/opt/graphite/conf
|
|
||||||
- ./data/statsd-config/config.json:/opt/statsd/config.js
|
|
@ -1,35 +0,0 @@
|
|||||||
version: '2'
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
build: .
|
|
||||||
image: registry.ely.by/elyby/skinsystem:latest
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
volumes:
|
|
||||||
- ./:/go/src/app
|
|
||||||
command: ["go", "run", "minecraft-skinsystem.go"]
|
|
||||||
links:
|
|
||||||
- redis
|
|
||||||
- rabbitmq
|
|
||||||
- statsd
|
|
||||||
environment:
|
|
||||||
ACCOUNTS_API_ID: ""
|
|
||||||
ACCOUNTS_API_SECRET: ""
|
|
||||||
STATSD_ADDR: ""
|
|
||||||
|
|
||||||
redis:
|
|
||||||
extends:
|
|
||||||
file: docker-compose.base.yml
|
|
||||||
service: redis
|
|
||||||
|
|
||||||
rabbitmq:
|
|
||||||
extends:
|
|
||||||
file: docker-compose.base.yml
|
|
||||||
service: rabbitmq
|
|
||||||
|
|
||||||
statsd:
|
|
||||||
extends:
|
|
||||||
file: docker-compose.base.yml
|
|
||||||
service: statsd
|
|
||||||
ports:
|
|
||||||
- "8123:80"
|
|
@ -1,26 +0,0 @@
|
|||||||
version: '2'
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
image: registry.ely.by/elyby/skinsystem:latest
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
links:
|
|
||||||
- redis
|
|
||||||
- rabbitmq
|
|
||||||
restart: always
|
|
||||||
environment:
|
|
||||||
ACCOUNTS_API_ID: ""
|
|
||||||
ACCOUNTS_API_SECRET: ""
|
|
||||||
STATSD_ADDR: ""
|
|
||||||
|
|
||||||
redis:
|
|
||||||
extends:
|
|
||||||
file: docker-compose.base.yml
|
|
||||||
service: redis
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
rabbitmq:
|
|
||||||
extends:
|
|
||||||
file: docker-compose.base.yml
|
|
||||||
service: rabbitmq
|
|
||||||
restart: always
|
|
9
docker/Dockerfile
Normal file
9
docker/Dockerfile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
FROM alpine:3.6
|
||||||
|
|
||||||
|
COPY docker/docker-entrypoint.sh /usr/local/bin/
|
||||||
|
COPY docker/config.dist.yml /usr/local/etc/minecraft-skinsystem/
|
||||||
|
|
||||||
|
COPY minecraft-skinsystem /usr/local/bin/
|
||||||
|
|
||||||
|
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||||
|
CMD ["serve"]
|
51
docker/config.dist.yml
Normal file
51
docker/config.dist.yml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Main server configuration. Actually you don't want to change it,
|
||||||
|
# but you able to change host or port, that will be used by serve command
|
||||||
|
server:
|
||||||
|
host: # leave host empty to allow Docker publish port
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
# Worker listen to AMQP events, so it should know how to connect to any
|
||||||
|
# AMQP provider (actually RabbitMQ). You should not escape any vhost
|
||||||
|
# characters, 'cause it will be done by application automatically
|
||||||
|
amqp:
|
||||||
|
host: rabbitmq
|
||||||
|
port: 5672
|
||||||
|
username: minecraft-skinsystem-app
|
||||||
|
password: minecraft-skinsystem-app-password
|
||||||
|
vhost: /
|
||||||
|
|
||||||
|
# Both of web or worker depends on storage.
|
||||||
|
storage:
|
||||||
|
# For now app require Redis and don't support any other backends to store
|
||||||
|
# skins, but in the future we can have more backends. Poll size tune amount
|
||||||
|
# of connections to the redis. It's not recommended to set it less then 2
|
||||||
|
# because it will lead to panic on high load.
|
||||||
|
redis:
|
||||||
|
host: redis
|
||||||
|
port: 6379
|
||||||
|
poolSize: 10
|
||||||
|
|
||||||
|
# Filesystem storage used to store capes. basePath specify absolute or relative
|
||||||
|
# path to storage and capesDirName specify which folder in this base path will
|
||||||
|
# be used to search capes.
|
||||||
|
filesystem:
|
||||||
|
basePath: /data
|
||||||
|
capesDirName: capes
|
||||||
|
|
||||||
|
# Accounts Ely.by internal API will be used in cases, when by some reasons
|
||||||
|
# information about user will be unavailable in the app storage.
|
||||||
|
api:
|
||||||
|
accounts:
|
||||||
|
host: https://account.ely.by
|
||||||
|
id: app-id
|
||||||
|
secret: secret
|
||||||
|
scopes:
|
||||||
|
- internal_account_info
|
||||||
|
|
||||||
|
# StatsD can be used to collect metrics
|
||||||
|
# statsd:
|
||||||
|
# addr: localhost:3746
|
||||||
|
|
||||||
|
# Sentry can be used to collect app errors
|
||||||
|
# sentry:
|
||||||
|
# dsn: https://public:private@your.sentry.io/1
|
46
docker/docker-compose.dev.yml
Normal file
46
docker/docker-compose.dev.yml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# This compose file contains necessary docker-compose config to quick start
|
||||||
|
# services required by app. Ports published to host.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# 1. Clone this file as docker-compose.yml:
|
||||||
|
# cp docker/docker-compose.dev.yml docker-compose.yml
|
||||||
|
#
|
||||||
|
# 2. If necessary, then you can fix configuration to your environment.
|
||||||
|
# Then start all services:
|
||||||
|
# docker-compose up -d
|
||||||
|
#
|
||||||
|
# 3. Pass to the project configuration links to this services:
|
||||||
|
# amqp:
|
||||||
|
# host: localhost
|
||||||
|
# port: 5672
|
||||||
|
# username: ely
|
||||||
|
# password: ely
|
||||||
|
# vhost: /ely
|
||||||
|
#
|
||||||
|
# storage:
|
||||||
|
# redis:
|
||||||
|
# host: localhost
|
||||||
|
# port: 6379
|
||||||
|
# poolSize: 10
|
||||||
|
#
|
||||||
|
# 4. After job is done all services can be stopped:
|
||||||
|
# docker-compose stop
|
||||||
|
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:3.2-32bit
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- ./data/redis:/data
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
image: rabbitmq:3.6-management-alpine
|
||||||
|
ports:
|
||||||
|
- "5672:5672"
|
||||||
|
- "15672:15672"
|
||||||
|
environment:
|
||||||
|
RABBITMQ_DEFAULT_USER: "ely"
|
||||||
|
RABBITMQ_DEFAULT_PASS: "ely"
|
||||||
|
RABBITMQ_DEFAULT_VHOST: "/ely"
|
36
docker/docker-compose.prod.yml
Normal file
36
docker/docker-compose.prod.yml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: registry.ely.by/elyby/skinsystem:latest
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
links:
|
||||||
|
- redis
|
||||||
|
volumes:
|
||||||
|
- ./data/capes:/data/capes
|
||||||
|
- ./config/minecraft-skinsystem:/etc/minecraft-skinsystem
|
||||||
|
|
||||||
|
worker:
|
||||||
|
image: registry.ely.by/elyby/skinsystem:latest
|
||||||
|
restart: always
|
||||||
|
links:
|
||||||
|
- redis
|
||||||
|
- rabbitmq
|
||||||
|
command: ["amqp-worker"]
|
||||||
|
volumes:
|
||||||
|
- ./config/minecraft-skinsystem:/etc/minecraft-skinsystem
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:3.2-32bit # 32-bit version used to decrease memory usage
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./data/redis:/data
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
image: rabbitmq:3.6-alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
RABBITMQ_DEFAULT_USER: minecraft-skinsystem-app
|
||||||
|
RABBITMQ_DEFAULT_PASS: minecraft-skinsystem-app-password
|
||||||
|
RABBITMQ_DEFAULT_VHOST: /
|
15
docker/docker-entrypoint.sh
Executable file
15
docker/docker-entrypoint.sh
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CONFIG="/etc/minecraft-skinsystem/config.yml"
|
||||||
|
|
||||||
|
if [ ! -f "$CONFIG" ]; then
|
||||||
|
mkdir -p $(dirname "${CONFIG}")
|
||||||
|
cp /usr/local/etc/minecraft-skinsystem/config.dist.yml "$CONFIG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" = "serve" ] || [ "$1" = "amqp-worker" ]; then
|
||||||
|
set -- minecraft-skinsystem "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
38
http/cape.go
Normal file
38
http/cape.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (cfg *Config) Cape(response http.ResponseWriter, request *http.Request) {
|
||||||
|
if mux.Vars(request)["converted"] == "" {
|
||||||
|
cfg.Logger.IncCounter("capes.request", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
username := parseUsername(mux.Vars(request)["username"])
|
||||||
|
rec, err := cfg.CapesRepo.FindByUsername(username)
|
||||||
|
if err != nil {
|
||||||
|
http.Redirect(response, request, "http://skins.minecraft.net/MinecraftCloaks/" + username + ".png", 301)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Set("Content-Type", "image/png")
|
||||||
|
io.Copy(response, rec.File)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) CapeGET(response http.ResponseWriter, request *http.Request) {
|
||||||
|
cfg.Logger.IncCounter("capes.get_request", 1)
|
||||||
|
username := request.URL.Query().Get("name")
|
||||||
|
if username == "" {
|
||||||
|
response.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.Vars(request)["username"] = username
|
||||||
|
mux.Vars(request)["converted"] = "1"
|
||||||
|
|
||||||
|
cfg.Cape(response, request)
|
||||||
|
}
|
138
http/cape_test.go
Normal file
138
http/cape_test.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/db"
|
||||||
|
"elyby/minecraft-skinsystem/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_Cape(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, _, capesRepo, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
cape := createCape()
|
||||||
|
|
||||||
|
capesRepo.EXPECT().FindByUsername("mocked_username").Return(&model.Cape{
|
||||||
|
File: bytes.NewReader(cape),
|
||||||
|
}, nil)
|
||||||
|
wd.EXPECT().IncCounter("capes.request", int64(1))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/mocked_username", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(200, resp.StatusCode)
|
||||||
|
responseData, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.Equal(cape, responseData)
|
||||||
|
assert.Equal("image/png", resp.Header.Get("Content-Type"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Cape2(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, _, capesRepo, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
capesRepo.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{"notch"})
|
||||||
|
wd.EXPECT().IncCounter("capes.request", int64(1))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/notch", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(301, resp.StatusCode)
|
||||||
|
assert.Equal("http://skins.minecraft.net/MinecraftCloaks/notch.png", resp.Header.Get("Location"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_CapeGET(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, _, capesRepo, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
cape := createCape()
|
||||||
|
|
||||||
|
capesRepo.EXPECT().FindByUsername("mocked_username").Return(&model.Cape{
|
||||||
|
File: bytes.NewReader(cape),
|
||||||
|
}, nil)
|
||||||
|
wd.EXPECT().IncCounter("capes.request", int64(1)).Times(0)
|
||||||
|
wd.EXPECT().IncCounter("capes.get_request", int64(1))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks?name=mocked_username", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(200, resp.StatusCode)
|
||||||
|
responseData, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.Equal(cape, responseData)
|
||||||
|
assert.Equal("image/png", resp.Header.Get("Content-Type"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_CapeGET2(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, _, capesRepo, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
capesRepo.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{"notch"})
|
||||||
|
wd.EXPECT().IncCounter("capes.request", int64(1)).Times(0)
|
||||||
|
wd.EXPECT().IncCounter("capes.get_request", int64(1))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks?name=notch", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(301, resp.StatusCode)
|
||||||
|
assert.Equal("http://skins.minecraft.net/MinecraftCloaks/notch.png", resp.Header.Get("Location"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_CapeGET3(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/?name=notch", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
(&Config{}).CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(301, resp.StatusCode)
|
||||||
|
assert.Equal("http://skinsystem.ely.by/cloaks?name=notch", resp.Header.Get("Location"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cape md5: 424ff79dce9940af89c28ad80de8aaad
|
||||||
|
func createCape() []byte {
|
||||||
|
img := image.NewAlpha(image.Rect(0, 0, 64, 32))
|
||||||
|
writer := &bytes.Buffer{}
|
||||||
|
png.Encode(writer, img)
|
||||||
|
|
||||||
|
pngBytes, _ := ioutil.ReadAll(writer)
|
||||||
|
|
||||||
|
return pngBytes
|
||||||
|
}
|
27
http/face.go
Normal file
27
http/face.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultHash = "default"
|
||||||
|
|
||||||
|
func (cfg *Config) Face(response http.ResponseWriter, request *http.Request) {
|
||||||
|
cfg.Logger.IncCounter("faces.request", 1)
|
||||||
|
username := parseUsername(mux.Vars(request)["username"])
|
||||||
|
rec, err := cfg.SkinsRepo.FindByUsername(username)
|
||||||
|
var hash string
|
||||||
|
if err != nil || rec.SkinId == 0 {
|
||||||
|
hash = defaultHash
|
||||||
|
} else {
|
||||||
|
hash = rec.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(response, request, buildElyUrl(buildFaceUrl(hash)), 301)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFaceUrl(hash string) string {
|
||||||
|
return "/minecraft/skin_buffer/faces/" + hash + ".png"
|
||||||
|
}
|
53
http/face_test.go
Normal file
53
http/face_test.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_Face(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, skinsRepo, _, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||||
|
wd.EXPECT().IncCounter("faces.request", int64(1))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user/face.png", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(301, resp.StatusCode)
|
||||||
|
assert.Equal("http://ely.by/minecraft/skin_buffer/faces/55d2a8848764f5ff04012cdb093458bd.png", resp.Header.Get("Location"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Face2(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, skinsRepo, _, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
skinsRepo.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{"mock_user"})
|
||||||
|
wd.EXPECT().IncCounter("faces.request", int64(1))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user/face.png", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(301, resp.StatusCode)
|
||||||
|
assert.Equal("http://ely.by/minecraft/skin_buffer/faces/default.png", resp.Header.Get("Location"))
|
||||||
|
}
|
91
http/http.go
Normal file
91
http/http.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/mono83/slf/wd"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/interfaces"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ListenSpec string
|
||||||
|
|
||||||
|
SkinsRepo interfaces.SkinsRepository
|
||||||
|
CapesRepo interfaces.CapesRepository
|
||||||
|
Logger wd.Watchdog
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) Run() error {
|
||||||
|
cfg.Logger.Info(fmt.Sprintf("Starting, HTTP on: %s\n", cfg.ListenSpec))
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", cfg.ListenSpec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
ReadTimeout: 60 * time.Second,
|
||||||
|
WriteTimeout: 60 * time.Second,
|
||||||
|
MaxHeaderBytes: 1 << 16,
|
||||||
|
Handler: cfg.CreateHandler(),
|
||||||
|
}
|
||||||
|
|
||||||
|
go server.Serve(listener)
|
||||||
|
|
||||||
|
s := waitForSignal()
|
||||||
|
cfg.Logger.Info(fmt.Sprintf("Got signal: %v, exiting.", s))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) CreateHandler() http.Handler {
|
||||||
|
router := mux.NewRouter().StrictSlash(true)
|
||||||
|
|
||||||
|
router.HandleFunc("/skins/{username}", cfg.Skin).Methods("GET")
|
||||||
|
router.HandleFunc("/cloaks/{username}", cfg.Cape).Methods("GET").Name("cloaks")
|
||||||
|
router.HandleFunc("/textures/{username}", cfg.Textures).Methods("GET")
|
||||||
|
router.HandleFunc("/textures/signed/{username}", cfg.SignedTextures).Methods("GET")
|
||||||
|
router.HandleFunc("/skins/{username}/face", cfg.Face).Methods("GET")
|
||||||
|
router.HandleFunc("/skins/{username}/face.png", cfg.Face).Methods("GET")
|
||||||
|
// Legacy
|
||||||
|
router.HandleFunc("/skins", cfg.SkinGET).Methods("GET")
|
||||||
|
router.HandleFunc("/cloaks", cfg.CapeGET).Methods("GET")
|
||||||
|
// 404
|
||||||
|
router.NotFoundHandler = http.HandlerFunc(cfg.NotFound)
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUsername(username string) string {
|
||||||
|
const suffix = ".png"
|
||||||
|
if strings.HasSuffix(username, suffix) {
|
||||||
|
username = strings.TrimSuffix(username, suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
return username
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildElyUrl(route string) string {
|
||||||
|
prefix := "http://ely.by"
|
||||||
|
if !strings.HasPrefix(route, prefix) {
|
||||||
|
route = prefix + route
|
||||||
|
}
|
||||||
|
|
||||||
|
return route
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForSignal() os.Signal {
|
||||||
|
ch := make(chan os.Signal)
|
||||||
|
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
return <-ch
|
||||||
|
}
|
40
http/http_test.go
Normal file
40
http/http_test.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/interfaces/mock_interfaces"
|
||||||
|
"elyby/minecraft-skinsystem/interfaces/mock_wd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseUsername(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
assert.Equal("test", parseUsername("test.png"), "Function should trim .png at end")
|
||||||
|
assert.Equal("test", parseUsername("test"), "Function should return string itself, if it not contains .png at end")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildElyUrl(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
assert.Equal("http://ely.by/route", buildElyUrl("/route"), "Function should add prefix to the provided relative url.")
|
||||||
|
assert.Equal("http://ely.by/test/route", buildElyUrl("http://ely.by/test/route"), "Function should do not add prefix to the provided prefixed url.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupMocks(ctrl *gomock.Controller) (
|
||||||
|
*Config,
|
||||||
|
*mock_interfaces.MockSkinsRepository,
|
||||||
|
*mock_interfaces.MockCapesRepository,
|
||||||
|
*mock_wd.MockWatchdog,
|
||||||
|
) {
|
||||||
|
skinsRepo := mock_interfaces.NewMockSkinsRepository(ctrl)
|
||||||
|
capesRepo := mock_interfaces.NewMockCapesRepository(ctrl)
|
||||||
|
wd := mock_wd.NewMockWatchdog(ctrl)
|
||||||
|
|
||||||
|
return &Config{
|
||||||
|
SkinsRepo: skinsRepo,
|
||||||
|
CapesRepo: capesRepo,
|
||||||
|
Logger: wd,
|
||||||
|
}, skinsRepo, capesRepo, wd
|
||||||
|
}
|
18
http/not_found.go
Normal file
18
http/not_found.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (cfg *Config) NotFound(response http.ResponseWriter, request *http.Request) {
|
||||||
|
data, _ := json.Marshal(map[string]string{
|
||||||
|
"status": "404",
|
||||||
|
"message": "Not Found",
|
||||||
|
"link": "http://docs.ely.by/skin-system.html",
|
||||||
|
})
|
||||||
|
|
||||||
|
response.Header().Set("Content-Type", "application/json")
|
||||||
|
response.WriteHeader(http.StatusNotFound)
|
||||||
|
response.Write(data)
|
||||||
|
}
|
28
http/not_found_test.go
Normal file
28
http/not_found_test.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_NotFound(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
(&Config{}).CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(404, resp.StatusCode)
|
||||||
|
assert.Equal("application/json", resp.Header.Get("Content-Type"))
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.JSONEq(`{
|
||||||
|
"status": "404",
|
||||||
|
"message": "Not Found",
|
||||||
|
"link": "http://docs.ely.by/skin-system.html"
|
||||||
|
}`, string(response))
|
||||||
|
}
|
53
http/signed_textures.go
Normal file
53
http/signed_textures.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
type signedTexturesResponse struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
IsEly bool `json:"ely,omitempty"`
|
||||||
|
Props []property `json:"properties"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type property struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Signature string `json:"signature,omitempty"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) SignedTextures(response http.ResponseWriter, request *http.Request) {
|
||||||
|
cfg.Logger.IncCounter("signed_textures.request", 1)
|
||||||
|
username := parseUsername(mux.Vars(request)["username"])
|
||||||
|
|
||||||
|
rec, err := cfg.SkinsRepo.FindByUsername(username)
|
||||||
|
if err != nil || rec.SkinId == 0 || rec.MojangTextures == "" {
|
||||||
|
response.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData:= signedTexturesResponse{
|
||||||
|
Id: strings.Replace(rec.Uuid, "-", "", -1),
|
||||||
|
Name: rec.Username,
|
||||||
|
Props: []property{
|
||||||
|
{
|
||||||
|
Name: "textures",
|
||||||
|
Signature: rec.MojangSignature,
|
||||||
|
Value: rec.MojangTextures,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ely",
|
||||||
|
Value: "but why are you asking?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
responseJson,_ := json.Marshal(responseData)
|
||||||
|
response.Header().Set("Content-Type", "application/json")
|
||||||
|
response.Write(responseJson)
|
||||||
|
}
|
71
http/signed_textures_test.go
Normal file
71
http/signed_textures_test.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_SignedTextures(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, skinsRepo, _, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||||
|
wd.EXPECT().IncCounter("signed_textures.request", int64(1))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/signed/mock_user", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(200, resp.StatusCode)
|
||||||
|
assert.Equal("application/json", resp.Header.Get("Content-Type"))
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.JSONEq(`{
|
||||||
|
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||||
|
"name": "mock_user",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "textures",
|
||||||
|
"signature": "mocked signature",
|
||||||
|
"value": "mocked textures base64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ely",
|
||||||
|
"value": "but why are you asking?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, string(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_SignedTextures2(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, skinsRepo, _, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
skinsRepo.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{})
|
||||||
|
wd.EXPECT().IncCounter("signed_textures.request", int64(1))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/signed/mock_user", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(204, resp.StatusCode)
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.Equal("", string(response))
|
||||||
|
}
|
36
http/skin.go
Normal file
36
http/skin.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (cfg *Config) Skin(response http.ResponseWriter, request *http.Request) {
|
||||||
|
if mux.Vars(request)["converted"] == "" {
|
||||||
|
cfg.Logger.IncCounter("skins.request", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
username := parseUsername(mux.Vars(request)["username"])
|
||||||
|
rec, err := cfg.SkinsRepo.FindByUsername(username)
|
||||||
|
if err != nil || rec.SkinId == 0 {
|
||||||
|
http.Redirect(response, request, "http://skins.minecraft.net/MinecraftSkins/" + username + ".png", 301)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(response, request, buildElyUrl(rec.Url), 301)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) SkinGET(response http.ResponseWriter, request *http.Request) {
|
||||||
|
cfg.Logger.IncCounter("skins.get_request", 1)
|
||||||
|
username := request.URL.Query().Get("name")
|
||||||
|
if username == "" {
|
||||||
|
response.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.Vars(request)["username"] = username
|
||||||
|
mux.Vars(request)["converted"] = "1"
|
||||||
|
|
||||||
|
cfg.Skin(response, request)
|
||||||
|
}
|
124
http/skin_test.go
Normal file
124
http/skin_test.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/db"
|
||||||
|
"elyby/minecraft-skinsystem/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_Skin(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, skinsRepo, _, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||||
|
wd.EXPECT().IncCounter("skins.request", int64(1))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(301, resp.StatusCode)
|
||||||
|
assert.Equal("http://ely.by/minecraft/skins/skin.png", resp.Header.Get("Location"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Skin2(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, skinsRepo, _, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
skinsRepo.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{"notch"})
|
||||||
|
wd.EXPECT().IncCounter("skins.request", int64(1))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/notch", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(301, resp.StatusCode)
|
||||||
|
assert.Equal("http://skins.minecraft.net/MinecraftSkins/notch.png", resp.Header.Get("Location"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_SkinGET(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, skinsRepo, _, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||||
|
wd.EXPECT().IncCounter("skins.get_request", int64(1))
|
||||||
|
wd.EXPECT().IncCounter("skins.request", int64(1)).Times(0)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins?name=mock_user", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(301, resp.StatusCode)
|
||||||
|
assert.Equal("http://ely.by/minecraft/skins/skin.png", resp.Header.Get("Location"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_SkinGET2(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, skinsRepo, _, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
skinsRepo.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{"notch"})
|
||||||
|
wd.EXPECT().IncCounter("skins.get_request", int64(1))
|
||||||
|
wd.EXPECT().IncCounter("skins.request", int64(1)).Times(0)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins?name=notch", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(301, resp.StatusCode)
|
||||||
|
assert.Equal("http://skins.minecraft.net/MinecraftSkins/notch.png", resp.Header.Get("Location"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_SkinGET3(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/?name=notch", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
(&Config{}).CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(301, resp.StatusCode)
|
||||||
|
assert.Equal("http://skinsystem.ely.by/skins?name=notch", resp.Header.Get("Location"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSkinModel(username string, isSlim bool) *model.Skin {
|
||||||
|
return &model.Skin{
|
||||||
|
Username: username,
|
||||||
|
Uuid: "0f657aa8-bfbe-415d-b700-5750090d3af3",
|
||||||
|
SkinId: 1,
|
||||||
|
Hash: "55d2a8848764f5ff04012cdb093458bd",
|
||||||
|
Url: "http://ely.by/minecraft/skins/skin.png",
|
||||||
|
MojangTextures: "mocked textures base64",
|
||||||
|
MojangSignature: "mocked signature",
|
||||||
|
IsSlim: isSlim,
|
||||||
|
}
|
||||||
|
}
|
104
http/textures.go
Normal file
104
http/textures.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type texturesResponse struct {
|
||||||
|
Skin *Skin `json:"SKIN"`
|
||||||
|
Cape *Cape `json:"CAPE,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Skin struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Metadata *skinMetadata `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type skinMetadata struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cape struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) Textures(response http.ResponseWriter, request *http.Request) {
|
||||||
|
cfg.Logger.IncCounter("textures.request", 1)
|
||||||
|
username := parseUsername(mux.Vars(request)["username"])
|
||||||
|
|
||||||
|
skin, err := cfg.SkinsRepo.FindByUsername(username)
|
||||||
|
if err != nil || skin.SkinId == 0 {
|
||||||
|
if skin == nil {
|
||||||
|
skin = &model.Skin{}
|
||||||
|
}
|
||||||
|
|
||||||
|
skin.Url = "http://skins.minecraft.net/MinecraftSkins/" + username + ".png"
|
||||||
|
skin.Hash = string(buildNonElyTexturesHash(username))
|
||||||
|
} else {
|
||||||
|
skin.Url = buildElyUrl(skin.Url)
|
||||||
|
}
|
||||||
|
|
||||||
|
textures := texturesResponse{
|
||||||
|
Skin: &Skin{
|
||||||
|
Url: skin.Url,
|
||||||
|
Hash: skin.Hash,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if skin.IsSlim {
|
||||||
|
textures.Skin.Metadata = &skinMetadata{
|
||||||
|
Model: "slim",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cape, err := cfg.CapesRepo.FindByUsername(username)
|
||||||
|
if err == nil {
|
||||||
|
var scheme string = "http://"
|
||||||
|
if request.TLS != nil {
|
||||||
|
scheme = "https://"
|
||||||
|
}
|
||||||
|
|
||||||
|
textures.Cape = &Cape{
|
||||||
|
Url: scheme + request.Host + "/cloaks/" + username,
|
||||||
|
Hash: calculateCapeHash(cape),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData, _ := json.Marshal(textures)
|
||||||
|
response.Header().Set("Content-Type", "application/json")
|
||||||
|
response.Write(responseData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateCapeHash(cape *model.Cape) string {
|
||||||
|
hasher := md5.New()
|
||||||
|
io.Copy(hasher, cape.File)
|
||||||
|
|
||||||
|
return hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildNonElyTexturesHash(username string) string {
|
||||||
|
hour := getCurrentHour()
|
||||||
|
hasher := md5.New()
|
||||||
|
hasher.Write([]byte("non-ely-" + strconv.FormatInt(hour, 10) + "-" + username))
|
||||||
|
|
||||||
|
return hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeNow = time.Now
|
||||||
|
|
||||||
|
func getCurrentHour() int64 {
|
||||||
|
n := timeNow()
|
||||||
|
return time.Date(n.Year(), n.Month(), n.Day(), n.Hour(), 0, 0, 0, time.UTC).Unix()
|
||||||
|
}
|
166
http/textures_test.go
Normal file
166
http/textures_test.go
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/db"
|
||||||
|
"elyby/minecraft-skinsystem/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_Textures(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, skinsRepo, capesRepo, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||||
|
capesRepo.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{"mock_user"})
|
||||||
|
wd.EXPECT().IncCounter("textures.request", int64(1))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/mock_user", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(200, resp.StatusCode)
|
||||||
|
assert.Equal("application/json", resp.Header.Get("Content-Type"))
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.JSONEq(`{
|
||||||
|
"SKIN": {
|
||||||
|
"url": "http://ely.by/minecraft/skins/skin.png",
|
||||||
|
"hash": "55d2a8848764f5ff04012cdb093458bd"
|
||||||
|
}
|
||||||
|
}`, string(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Textures2(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, skinsRepo, capesRepo, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", true), nil)
|
||||||
|
capesRepo.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{"mock_user"})
|
||||||
|
wd.EXPECT().IncCounter("textures.request", int64(1))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/mock_user", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(200, resp.StatusCode)
|
||||||
|
assert.Equal("application/json", resp.Header.Get("Content-Type"))
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.JSONEq(`{
|
||||||
|
"SKIN": {
|
||||||
|
"url": "http://ely.by/minecraft/skins/skin.png",
|
||||||
|
"hash": "55d2a8848764f5ff04012cdb093458bd",
|
||||||
|
"metadata": {
|
||||||
|
"model": "slim"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`, string(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Textures3(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, skinsRepo, capesRepo, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||||
|
capesRepo.EXPECT().FindByUsername("mock_user").Return(&model.Cape{
|
||||||
|
File: bytes.NewReader(createCape()),
|
||||||
|
}, nil)
|
||||||
|
wd.EXPECT().IncCounter("textures.request", int64(1))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/mock_user", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(200, resp.StatusCode)
|
||||||
|
assert.Equal("application/json", resp.Header.Get("Content-Type"))
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.JSONEq(`{
|
||||||
|
"SKIN": {
|
||||||
|
"url": "http://ely.by/minecraft/skins/skin.png",
|
||||||
|
"hash": "55d2a8848764f5ff04012cdb093458bd"
|
||||||
|
},
|
||||||
|
"CAPE": {
|
||||||
|
"url": "http://skinsystem.ely.by/cloaks/mock_user",
|
||||||
|
"hash": "424ff79dce9940af89c28ad80de8aaad"
|
||||||
|
}
|
||||||
|
}`, string(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Textures4(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, skinsRepo, capesRepo, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
skinsRepo.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{})
|
||||||
|
capesRepo.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{})
|
||||||
|
wd.EXPECT().IncCounter("textures.request", int64(1))
|
||||||
|
timeNow = func() time.Time {
|
||||||
|
return time.Date(2017, time.August, 20, 0, 15, 54, 0, time.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/notch", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(200, resp.StatusCode)
|
||||||
|
assert.Equal("application/json", resp.Header.Get("Content-Type"))
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.JSONEq(`{
|
||||||
|
"SKIN": {
|
||||||
|
"url": "http://skins.minecraft.net/MinecraftSkins/notch.png",
|
||||||
|
"hash": "5923cf3f7fa170a279e4d7a9483cfc52"
|
||||||
|
}
|
||||||
|
}`, string(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildNonElyTexturesHash(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
timeNow = func() time.Time {
|
||||||
|
return time.Date(2017, time.November, 30, 16, 15, 34, 0, time.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal("686d788a5353cb636e8fdff727634d88", buildNonElyTexturesHash("username"), "Function should return fixed hash by username-time pair")
|
||||||
|
assert.Equal("fb876f761683a10accdb17d403cef64c", buildNonElyTexturesHash("another-username"), "Function should return fixed hash by username-time pair")
|
||||||
|
|
||||||
|
timeNow = func() time.Time {
|
||||||
|
return time.Date(2017, time.November, 30, 16, 20, 12, 0, time.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal("686d788a5353cb636e8fdff727634d88", buildNonElyTexturesHash("username"), "Function should do not change it's value if hour the same")
|
||||||
|
assert.Equal("fb876f761683a10accdb17d403cef64c", buildNonElyTexturesHash("another-username"), "Function should return fixed hash by username-time pair")
|
||||||
|
|
||||||
|
timeNow = func() time.Time {
|
||||||
|
return time.Date(2017, time.November, 30, 17, 1, 3, 0, time.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal("42277892fd24bc0ed86285b3bb8b8fad", buildNonElyTexturesHash("username"), "Function should change it's value if hour changed")
|
||||||
|
}
|
9
interfaces/api.go
Normal file
9
interfaces/api.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"elyby/minecraft-skinsystem/api/accounts"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccountsAPI interface {
|
||||||
|
AccountInfo(attribute string, value string) (*accounts.AccountInfoResponse, error)
|
||||||
|
}
|
46
interfaces/mock_interfaces/mock_api.go
Normal file
46
interfaces/mock_interfaces/mock_api.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: interfaces/api.go
|
||||||
|
|
||||||
|
package mock_interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
accounts "elyby/minecraft-skinsystem/api/accounts"
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
reflect "reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockAccountsAPI is a mock of AccountsAPI interface
|
||||||
|
type MockAccountsAPI struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockAccountsAPIMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockAccountsAPIMockRecorder is the mock recorder for MockAccountsAPI
|
||||||
|
type MockAccountsAPIMockRecorder struct {
|
||||||
|
mock *MockAccountsAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockAccountsAPI creates a new mock instance
|
||||||
|
func NewMockAccountsAPI(ctrl *gomock.Controller) *MockAccountsAPI {
|
||||||
|
mock := &MockAccountsAPI{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockAccountsAPIMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use
|
||||||
|
func (_m *MockAccountsAPI) EXPECT() *MockAccountsAPIMockRecorder {
|
||||||
|
return _m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountInfo mocks base method
|
||||||
|
func (_m *MockAccountsAPI) AccountInfo(attribute string, value string) (*accounts.AccountInfoResponse, error) {
|
||||||
|
ret := _m.ctrl.Call(_m, "AccountInfo", attribute, value)
|
||||||
|
ret0, _ := ret[0].(*accounts.AccountInfoResponse)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountInfo indicates an expected call of AccountInfo
|
||||||
|
func (_mr *MockAccountsAPIMockRecorder) AccountInfo(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "AccountInfo", reflect.TypeOf((*MockAccountsAPI)(nil).AccountInfo), arg0, arg1)
|
||||||
|
}
|
107
interfaces/mock_interfaces/mock_interfaces.go
Normal file
107
interfaces/mock_interfaces/mock_interfaces.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: interfaces/repositories.go
|
||||||
|
|
||||||
|
package mock_interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
model "elyby/minecraft-skinsystem/model"
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
reflect "reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockSkinsRepository is a mock of SkinsRepository interface
|
||||||
|
type MockSkinsRepository struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockSkinsRepositoryMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockSkinsRepositoryMockRecorder is the mock recorder for MockSkinsRepository
|
||||||
|
type MockSkinsRepositoryMockRecorder struct {
|
||||||
|
mock *MockSkinsRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockSkinsRepository creates a new mock instance
|
||||||
|
func NewMockSkinsRepository(ctrl *gomock.Controller) *MockSkinsRepository {
|
||||||
|
mock := &MockSkinsRepository{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockSkinsRepositoryMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use
|
||||||
|
func (_m *MockSkinsRepository) EXPECT() *MockSkinsRepositoryMockRecorder {
|
||||||
|
return _m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByUsername mocks base method
|
||||||
|
func (_m *MockSkinsRepository) FindByUsername(username string) (*model.Skin, error) {
|
||||||
|
ret := _m.ctrl.Call(_m, "FindByUsername", username)
|
||||||
|
ret0, _ := ret[0].(*model.Skin)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByUsername indicates an expected call of FindByUsername
|
||||||
|
func (_mr *MockSkinsRepositoryMockRecorder) FindByUsername(arg0 interface{}) *gomock.Call {
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "FindByUsername", reflect.TypeOf((*MockSkinsRepository)(nil).FindByUsername), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByUserId mocks base method
|
||||||
|
func (_m *MockSkinsRepository) FindByUserId(id int) (*model.Skin, error) {
|
||||||
|
ret := _m.ctrl.Call(_m, "FindByUserId", id)
|
||||||
|
ret0, _ := ret[0].(*model.Skin)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByUserId indicates an expected call of FindByUserId
|
||||||
|
func (_mr *MockSkinsRepositoryMockRecorder) FindByUserId(arg0 interface{}) *gomock.Call {
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "FindByUserId", reflect.TypeOf((*MockSkinsRepository)(nil).FindByUserId), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save mocks base method
|
||||||
|
func (_m *MockSkinsRepository) Save(skin *model.Skin) error {
|
||||||
|
ret := _m.ctrl.Call(_m, "Save", skin)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save indicates an expected call of Save
|
||||||
|
func (_mr *MockSkinsRepositoryMockRecorder) Save(arg0 interface{}) *gomock.Call {
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Save", reflect.TypeOf((*MockSkinsRepository)(nil).Save), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockCapesRepository is a mock of CapesRepository interface
|
||||||
|
type MockCapesRepository struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockCapesRepositoryMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockCapesRepositoryMockRecorder is the mock recorder for MockCapesRepository
|
||||||
|
type MockCapesRepositoryMockRecorder struct {
|
||||||
|
mock *MockCapesRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockCapesRepository creates a new mock instance
|
||||||
|
func NewMockCapesRepository(ctrl *gomock.Controller) *MockCapesRepository {
|
||||||
|
mock := &MockCapesRepository{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockCapesRepositoryMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use
|
||||||
|
func (_m *MockCapesRepository) EXPECT() *MockCapesRepositoryMockRecorder {
|
||||||
|
return _m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByUsername mocks base method
|
||||||
|
func (_m *MockCapesRepository) FindByUsername(username string) (*model.Cape, error) {
|
||||||
|
ret := _m.ctrl.Call(_m, "FindByUsername", username)
|
||||||
|
ret0, _ := ret[0].(*model.Cape)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByUsername indicates an expected call of FindByUsername
|
||||||
|
func (_mr *MockCapesRepositoryMockRecorder) FindByUsername(arg0 interface{}) *gomock.Call {
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "FindByUsername", reflect.TypeOf((*MockCapesRepository)(nil).FindByUsername), arg0)
|
||||||
|
}
|
218
interfaces/mock_wd/mock_wd.go
Normal file
218
interfaces/mock_wd/mock_wd.go
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: github.com/mono83/slf/wd (interfaces: Watchdog)
|
||||||
|
|
||||||
|
package mock_wd
|
||||||
|
|
||||||
|
import (
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
slf "github.com/mono83/slf"
|
||||||
|
wd "github.com/mono83/slf/wd"
|
||||||
|
reflect "reflect"
|
||||||
|
time "time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockWatchdog is a mock of Watchdog interface
|
||||||
|
type MockWatchdog struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockWatchdogMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockWatchdogMockRecorder is the mock recorder for MockWatchdog
|
||||||
|
type MockWatchdogMockRecorder struct {
|
||||||
|
mock *MockWatchdog
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockWatchdog creates a new mock instance
|
||||||
|
func NewMockWatchdog(ctrl *gomock.Controller) *MockWatchdog {
|
||||||
|
mock := &MockWatchdog{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockWatchdogMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use
|
||||||
|
func (_m *MockWatchdog) EXPECT() *MockWatchdogMockRecorder {
|
||||||
|
return _m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alert mocks base method
|
||||||
|
func (_m *MockWatchdog) Alert(_param0 string, _param1 ...slf.Param) {
|
||||||
|
_s := []interface{}{_param0}
|
||||||
|
for _, _x := range _param1 {
|
||||||
|
_s = append(_s, _x)
|
||||||
|
}
|
||||||
|
_m.ctrl.Call(_m, "Alert", _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alert indicates an expected call of Alert
|
||||||
|
func (_mr *MockWatchdogMockRecorder) Alert(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||||
|
_s := append([]interface{}{arg0}, arg1...)
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Alert", reflect.TypeOf((*MockWatchdog)(nil).Alert), _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug mocks base method
|
||||||
|
func (_m *MockWatchdog) Debug(_param0 string, _param1 ...slf.Param) {
|
||||||
|
_s := []interface{}{_param0}
|
||||||
|
for _, _x := range _param1 {
|
||||||
|
_s = append(_s, _x)
|
||||||
|
}
|
||||||
|
_m.ctrl.Call(_m, "Debug", _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug indicates an expected call of Debug
|
||||||
|
func (_mr *MockWatchdogMockRecorder) Debug(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||||
|
_s := append([]interface{}{arg0}, arg1...)
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Debug", reflect.TypeOf((*MockWatchdog)(nil).Debug), _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emergency mocks base method
|
||||||
|
func (_m *MockWatchdog) Emergency(_param0 string, _param1 ...slf.Param) {
|
||||||
|
_s := []interface{}{_param0}
|
||||||
|
for _, _x := range _param1 {
|
||||||
|
_s = append(_s, _x)
|
||||||
|
}
|
||||||
|
_m.ctrl.Call(_m, "Emergency", _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emergency indicates an expected call of Emergency
|
||||||
|
func (_mr *MockWatchdogMockRecorder) Emergency(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||||
|
_s := append([]interface{}{arg0}, arg1...)
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Emergency", reflect.TypeOf((*MockWatchdog)(nil).Emergency), _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error mocks base method
|
||||||
|
func (_m *MockWatchdog) Error(_param0 string, _param1 ...slf.Param) {
|
||||||
|
_s := []interface{}{_param0}
|
||||||
|
for _, _x := range _param1 {
|
||||||
|
_s = append(_s, _x)
|
||||||
|
}
|
||||||
|
_m.ctrl.Call(_m, "Error", _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error indicates an expected call of Error
|
||||||
|
func (_mr *MockWatchdogMockRecorder) Error(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||||
|
_s := append([]interface{}{arg0}, arg1...)
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Error", reflect.TypeOf((*MockWatchdog)(nil).Error), _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncCounter mocks base method
|
||||||
|
func (_m *MockWatchdog) IncCounter(_param0 string, _param1 int64, _param2 ...slf.Param) {
|
||||||
|
_s := []interface{}{_param0, _param1}
|
||||||
|
for _, _x := range _param2 {
|
||||||
|
_s = append(_s, _x)
|
||||||
|
}
|
||||||
|
_m.ctrl.Call(_m, "IncCounter", _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncCounter indicates an expected call of IncCounter
|
||||||
|
func (_mr *MockWatchdogMockRecorder) IncCounter(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
|
||||||
|
_s := append([]interface{}{arg0, arg1}, arg2...)
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "IncCounter", reflect.TypeOf((*MockWatchdog)(nil).IncCounter), _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info mocks base method
|
||||||
|
func (_m *MockWatchdog) Info(_param0 string, _param1 ...slf.Param) {
|
||||||
|
_s := []interface{}{_param0}
|
||||||
|
for _, _x := range _param1 {
|
||||||
|
_s = append(_s, _x)
|
||||||
|
}
|
||||||
|
_m.ctrl.Call(_m, "Info", _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info indicates an expected call of Info
|
||||||
|
func (_mr *MockWatchdogMockRecorder) Info(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||||
|
_s := append([]interface{}{arg0}, arg1...)
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Info", reflect.TypeOf((*MockWatchdog)(nil).Info), _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordTimer mocks base method
|
||||||
|
func (_m *MockWatchdog) RecordTimer(_param0 string, _param1 time.Duration, _param2 ...slf.Param) {
|
||||||
|
_s := []interface{}{_param0, _param1}
|
||||||
|
for _, _x := range _param2 {
|
||||||
|
_s = append(_s, _x)
|
||||||
|
}
|
||||||
|
_m.ctrl.Call(_m, "RecordTimer", _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordTimer indicates an expected call of RecordTimer
|
||||||
|
func (_mr *MockWatchdogMockRecorder) RecordTimer(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
|
||||||
|
_s := append([]interface{}{arg0, arg1}, arg2...)
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "RecordTimer", reflect.TypeOf((*MockWatchdog)(nil).RecordTimer), _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timer mocks base method
|
||||||
|
func (_m *MockWatchdog) Timer(_param0 string, _param1 ...slf.Param) slf.Timer {
|
||||||
|
_s := []interface{}{_param0}
|
||||||
|
for _, _x := range _param1 {
|
||||||
|
_s = append(_s, _x)
|
||||||
|
}
|
||||||
|
ret := _m.ctrl.Call(_m, "Timer", _s...)
|
||||||
|
ret0, _ := ret[0].(slf.Timer)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timer indicates an expected call of Timer
|
||||||
|
func (_mr *MockWatchdogMockRecorder) Timer(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||||
|
_s := append([]interface{}{arg0}, arg1...)
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Timer", reflect.TypeOf((*MockWatchdog)(nil).Timer), _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trace mocks base method
|
||||||
|
func (_m *MockWatchdog) Trace(_param0 string, _param1 ...slf.Param) {
|
||||||
|
_s := []interface{}{_param0}
|
||||||
|
for _, _x := range _param1 {
|
||||||
|
_s = append(_s, _x)
|
||||||
|
}
|
||||||
|
_m.ctrl.Call(_m, "Trace", _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trace indicates an expected call of Trace
|
||||||
|
func (_mr *MockWatchdogMockRecorder) Trace(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||||
|
_s := append([]interface{}{arg0}, arg1...)
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Trace", reflect.TypeOf((*MockWatchdog)(nil).Trace), _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateGauge mocks base method
|
||||||
|
func (_m *MockWatchdog) UpdateGauge(_param0 string, _param1 int64, _param2 ...slf.Param) {
|
||||||
|
_s := []interface{}{_param0, _param1}
|
||||||
|
for _, _x := range _param2 {
|
||||||
|
_s = append(_s, _x)
|
||||||
|
}
|
||||||
|
_m.ctrl.Call(_m, "UpdateGauge", _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateGauge indicates an expected call of UpdateGauge
|
||||||
|
func (_mr *MockWatchdogMockRecorder) UpdateGauge(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
|
||||||
|
_s := append([]interface{}{arg0, arg1}, arg2...)
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "UpdateGauge", reflect.TypeOf((*MockWatchdog)(nil).UpdateGauge), _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning mocks base method
|
||||||
|
func (_m *MockWatchdog) Warning(_param0 string, _param1 ...slf.Param) {
|
||||||
|
_s := []interface{}{_param0}
|
||||||
|
for _, _x := range _param1 {
|
||||||
|
_s = append(_s, _x)
|
||||||
|
}
|
||||||
|
_m.ctrl.Call(_m, "Warning", _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning indicates an expected call of Warning
|
||||||
|
func (_mr *MockWatchdogMockRecorder) Warning(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||||
|
_s := append([]interface{}{arg0}, arg1...)
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Warning", reflect.TypeOf((*MockWatchdog)(nil).Warning), _s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithParams mocks base method
|
||||||
|
func (_m *MockWatchdog) WithParams(_param0 ...slf.Param) wd.Watchdog {
|
||||||
|
_s := []interface{}{}
|
||||||
|
for _, _x := range _param0 {
|
||||||
|
_s = append(_s, _x)
|
||||||
|
}
|
||||||
|
ret := _m.ctrl.Call(_m, "WithParams", _s...)
|
||||||
|
ret0, _ := ret[0].(wd.Watchdog)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithParams indicates an expected call of WithParams
|
||||||
|
func (_mr *MockWatchdogMockRecorder) WithParams(arg0 ...interface{}) *gomock.Call {
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "WithParams", reflect.TypeOf((*MockWatchdog)(nil).WithParams), arg0...)
|
||||||
|
}
|
15
interfaces/repositories.go
Normal file
15
interfaces/repositories.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"elyby/minecraft-skinsystem/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SkinsRepository interface {
|
||||||
|
FindByUsername(username string) (*model.Skin, error)
|
||||||
|
FindByUserId(id int) (*model.Skin, error)
|
||||||
|
Save(skin *model.Skin) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type CapesRepository interface {
|
||||||
|
FindByUsername(username string) (*model.Cape, error)
|
||||||
|
}
|
@ -1,43 +0,0 @@
|
|||||||
package data
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/lib/services"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CapeItem struct {
|
|
||||||
File *os.File
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindCapeByUsername(username string) (CapeItem, error) {
|
|
||||||
var record CapeItem
|
|
||||||
file, err := os.Open(services.RootFolder + "/data/capes/" + strings.ToLower(username) + ".png")
|
|
||||||
if (err != nil) {
|
|
||||||
return record, CapeNotFound{username}
|
|
||||||
}
|
|
||||||
|
|
||||||
record.File = file
|
|
||||||
|
|
||||||
return record, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cape *CapeItem) CalculateHash() string {
|
|
||||||
hasher := md5.New()
|
|
||||||
io.Copy(hasher, cape.File)
|
|
||||||
|
|
||||||
return hex.EncodeToString(hasher.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
type CapeNotFound struct {
|
|
||||||
Who string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e CapeNotFound) Error() string {
|
|
||||||
return fmt.Sprintf("Cape file not found. Required username \"%v\"", e.Who)
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
package data
|
|
||||||
|
|
||||||
type SignedTexturesResponse struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
IsEly bool `json:"ely,omitempty"`
|
|
||||||
Props []Property `json:"properties"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Property struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Signature string `json:"signature,omitempty"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
@ -1,117 +0,0 @@
|
|||||||
package data
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"fmt"
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/lib/services"
|
|
||||||
"elyby/minecraft-skinsystem/lib/tools"
|
|
||||||
|
|
||||||
"github.com/mediocregopher/radix.v2/redis"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SkinItem struct {
|
|
||||||
UserId int `json:"userId"`
|
|
||||||
Uuid string `json:"uuid"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
SkinId int `json:"skinId"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
Is1_8 bool `json:"is1_8"`
|
|
||||||
IsSlim bool `json:"isSlim"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
MojangTextures string `json:"mojangTextures"`
|
|
||||||
MojangSignature string `json:"mojangSignature"`
|
|
||||||
oldUsername string
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountIdToUsernameKey string = "hash:username-to-account-id"
|
|
||||||
|
|
||||||
func (s *SkinItem) Save() {
|
|
||||||
str, _ := json.Marshal(s)
|
|
||||||
compressedStr := tools.ZlibEncode(str)
|
|
||||||
pool, _ := services.RedisPool.Get()
|
|
||||||
pool.Cmd("MULTI")
|
|
||||||
|
|
||||||
// Если пользователь сменил ник, то мы должны удать его ключ
|
|
||||||
if (s.oldUsername != "" && s.oldUsername != s.Username) {
|
|
||||||
pool.Cmd("DEL", tools.BuildKey(s.oldUsername))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если это новая запись или если пользователь сменил ник, то обновляем значение в хэш-таблице
|
|
||||||
if (s.oldUsername != "" || s.oldUsername != s.Username) {
|
|
||||||
pool.Cmd("HSET", accountIdToUsernameKey, s.UserId, s.Username)
|
|
||||||
}
|
|
||||||
|
|
||||||
pool.Cmd("SET", tools.BuildKey(s.Username), compressedStr)
|
|
||||||
|
|
||||||
pool.Cmd("EXEC")
|
|
||||||
|
|
||||||
s.oldUsername = s.Username
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SkinItem) Delete() {
|
|
||||||
if (s.oldUsername == "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pool, _ := services.RedisPool.Get()
|
|
||||||
pool.Cmd("MULTI")
|
|
||||||
|
|
||||||
pool.Cmd("DEL", tools.BuildKey(s.oldUsername))
|
|
||||||
pool.Cmd("HDEL", accountIdToUsernameKey, s.UserId)
|
|
||||||
|
|
||||||
pool.Cmd("EXEC")
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindSkinByUsername(username string) (SkinItem, error) {
|
|
||||||
var record SkinItem;
|
|
||||||
services.Logger.IncCounter("storage.query", 1)
|
|
||||||
redisKey := tools.BuildKey(username)
|
|
||||||
response := services.RedisPool.Cmd("GET", redisKey);
|
|
||||||
if (response.IsType(redis.Nil)) {
|
|
||||||
services.Logger.IncCounter("storage.not_found", 1)
|
|
||||||
return record, SkinNotFound{username}
|
|
||||||
}
|
|
||||||
|
|
||||||
encodedResult, err := response.Bytes()
|
|
||||||
if err == nil {
|
|
||||||
services.Logger.IncCounter("storage.found", 1)
|
|
||||||
result, err := tools.ZlibDecode(encodedResult)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Cannot uncompress zlib for key " + redisKey)
|
|
||||||
goto finish
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(result, &record)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Cannot decode record data for key" + redisKey)
|
|
||||||
goto finish
|
|
||||||
}
|
|
||||||
|
|
||||||
record.oldUsername = record.Username
|
|
||||||
}
|
|
||||||
|
|
||||||
finish:
|
|
||||||
|
|
||||||
return record, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindSkinById(id int) (SkinItem, error) {
|
|
||||||
response := services.RedisPool.Cmd("HGET", accountIdToUsernameKey, id);
|
|
||||||
if (response.IsType(redis.Nil)) {
|
|
||||||
return SkinItem{}, SkinNotFound{"unknown"}
|
|
||||||
}
|
|
||||||
|
|
||||||
username, _ := response.Str()
|
|
||||||
|
|
||||||
return FindSkinByUsername(username)
|
|
||||||
}
|
|
||||||
|
|
||||||
type SkinNotFound struct {
|
|
||||||
Who string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e SkinNotFound) Error() string {
|
|
||||||
return fmt.Sprintf("Skin data not found. Required username \"%v\"", e.Who)
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
package data
|
|
||||||
|
|
||||||
type TexturesResponse struct {
|
|
||||||
Skin *Skin `json:"SKIN"`
|
|
||||||
Cape *Cape `json:"CAPE,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Skin struct {
|
|
||||||
Url string `json:"url"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
Metadata *SkinMetadata `json:"metadata,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SkinMetadata struct {
|
|
||||||
Model string `json:"model"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Cape struct {
|
|
||||||
Url string `json:"url"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
}
|
|
44
lib/external/accounts/AccountInfo.go
vendored
44
lib/external/accounts/AccountInfo.go
vendored
@ -1,44 +0,0 @@
|
|||||||
package accounts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"io/ioutil"
|
|
||||||
"encoding/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AccountInfoResponse struct {
|
|
||||||
Id int `json:"id"`
|
|
||||||
Uuid string `json:"uuid"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const internalAccountInfoUrl = domain + "/api/internal/accounts/info"
|
|
||||||
|
|
||||||
func (token *Token) AccountInfo(attribute string, value string) (AccountInfoResponse, error) {
|
|
||||||
request, err := http.NewRequest("GET", internalAccountInfoUrl, nil)
|
|
||||||
request.Header.Add("Authorization", "Bearer " + token.AccessToken)
|
|
||||||
query := request.URL.Query()
|
|
||||||
query.Add(attribute, value)
|
|
||||||
request.URL.RawQuery = query.Encode()
|
|
||||||
|
|
||||||
response, err := Client.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
var info AccountInfoResponse
|
|
||||||
|
|
||||||
responseError := handleResponse(response)
|
|
||||||
if responseError != nil {
|
|
||||||
return info, responseError
|
|
||||||
}
|
|
||||||
|
|
||||||
body, _ := ioutil.ReadAll(response.Body)
|
|
||||||
println("Raw account info response is " + string(body))
|
|
||||||
json.Unmarshal(body, &info)
|
|
||||||
|
|
||||||
return info, nil
|
|
||||||
}
|
|
49
lib/external/accounts/GetToken.go
vendored
49
lib/external/accounts/GetToken.go
vendored
@ -1,49 +0,0 @@
|
|||||||
package accounts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"net/url"
|
|
||||||
"io/ioutil"
|
|
||||||
"encoding/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TokenRequest struct {
|
|
||||||
Id string
|
|
||||||
Secret string
|
|
||||||
Scopes []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Token struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
TokenType string `json:"token_type"`
|
|
||||||
ExpiresIn int `json:"expires_in"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenUrl = domain + "/api/oauth2/v1/token"
|
|
||||||
|
|
||||||
func GetToken(request TokenRequest) (Token, error) {
|
|
||||||
form := url.Values{}
|
|
||||||
form.Add("client_id", request.Id)
|
|
||||||
form.Add("client_secret", request.Secret)
|
|
||||||
form.Add("grant_type", "client_credentials")
|
|
||||||
form.Add("scope", strings.Join(request.Scopes, ","))
|
|
||||||
|
|
||||||
response, err := Client.Post(tokenUrl, "application/x-www-form-urlencoded", strings.NewReader(form.Encode()))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
var result Token
|
|
||||||
responseError := handleResponse(response)
|
|
||||||
if responseError != nil {
|
|
||||||
return result, responseError
|
|
||||||
}
|
|
||||||
|
|
||||||
body, _ := ioutil.ReadAll(response.Body)
|
|
||||||
|
|
||||||
json.Unmarshal(body, &result)
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
51
lib/external/accounts/base.go
vendored
51
lib/external/accounts/base.go
vendored
@ -1,51 +0,0 @@
|
|||||||
package accounts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
const domain = "https://account.ely.by"
|
|
||||||
|
|
||||||
var Client = &http.Client{}
|
|
||||||
|
|
||||||
type UnauthorizedResponse struct {}
|
|
||||||
|
|
||||||
func (err UnauthorizedResponse) Error() string {
|
|
||||||
return "Unauthorized response"
|
|
||||||
}
|
|
||||||
|
|
||||||
type ForbiddenResponse struct {}
|
|
||||||
|
|
||||||
func (err ForbiddenResponse) Error() string {
|
|
||||||
return "Forbidden response"
|
|
||||||
}
|
|
||||||
|
|
||||||
type NotFoundResponse struct {}
|
|
||||||
|
|
||||||
func (err NotFoundResponse) Error() string {
|
|
||||||
return "Not found"
|
|
||||||
}
|
|
||||||
|
|
||||||
type NotSuccessResponse struct {
|
|
||||||
StatusCode int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err NotSuccessResponse) Error() string {
|
|
||||||
return fmt.Sprintf("Response code is \"%d\"", err.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleResponse(response *http.Response) error {
|
|
||||||
switch status := response.StatusCode; status {
|
|
||||||
case 200:
|
|
||||||
return nil
|
|
||||||
case 401:
|
|
||||||
return &UnauthorizedResponse{}
|
|
||||||
case 403:
|
|
||||||
return &ForbiddenResponse{}
|
|
||||||
case 404:
|
|
||||||
return &NotFoundResponse{}
|
|
||||||
default:
|
|
||||||
return &NotSuccessResponse{status}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/lib/tools"
|
|
||||||
"elyby/minecraft-skinsystem/lib/data"
|
|
||||||
"elyby/minecraft-skinsystem/lib/services"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Cape(response http.ResponseWriter, request *http.Request) {
|
|
||||||
if (mux.Vars(request)["converted"] == "") {
|
|
||||||
services.Logger.IncCounter("capes.request", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
username := tools.ParseUsername(mux.Vars(request)["username"])
|
|
||||||
rec, err := data.FindCapeByUsername(username)
|
|
||||||
if (err != nil) {
|
|
||||||
http.Redirect(response, request, "http://skins.minecraft.net/MinecraftCloaks/" + username + ".png", 301)
|
|
||||||
}
|
|
||||||
|
|
||||||
request.Header.Set("Content-Type", "image/png")
|
|
||||||
io.Copy(response, rec.File)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CapeGET(w http.ResponseWriter, r *http.Request) {
|
|
||||||
services.Logger.IncCounter("capes.get_request", 1)
|
|
||||||
username := r.URL.Query().Get("name")
|
|
||||||
if username == "" {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mux.Vars(r)["username"] = username
|
|
||||||
mux.Vars(r)["converted"] = "1"
|
|
||||||
Cape(w, r)
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/lib/tools"
|
|
||||||
"elyby/minecraft-skinsystem/lib/data"
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultHash = "default"
|
|
||||||
|
|
||||||
func Face(w http.ResponseWriter, r *http.Request) {
|
|
||||||
username := tools.ParseUsername(mux.Vars(r)["username"])
|
|
||||||
rec, err := data.FindSkinByUsername(username)
|
|
||||||
var hash string
|
|
||||||
if (err != nil || rec.SkinId == 0) {
|
|
||||||
hash = defaultHash;
|
|
||||||
} else {
|
|
||||||
hash = rec.Hash
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(w, r, tools.BuildElyUrl(buildFaceUrl(hash)), 301);
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildFaceUrl(hash string) string {
|
|
||||||
return "/minecraft/skin_buffer/faces/" + hash + ".png"
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/lib/services"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Метод-наследие от первой версии системы скинов.
|
|
||||||
// Всё ещё иногда используется
|
|
||||||
// Просто конвертируем данные и отправляем их в основной обработчик
|
|
||||||
func MinecraftPHP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
username := r.URL.Query().Get("name")
|
|
||||||
required := r.URL.Query().Get("type")
|
|
||||||
if username == "" || required == "" {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mux.Vars(r)["username"] = username
|
|
||||||
mux.Vars(r)["converted"] = "1"
|
|
||||||
switch required {
|
|
||||||
case "skin":
|
|
||||||
services.Logger.IncCounter("skins.minecraft-php-request", 1)
|
|
||||||
Skin(w, r)
|
|
||||||
case "cloack":
|
|
||||||
services.Logger.IncCounter("capes.minecraft-php-request", 1)
|
|
||||||
Cape(w, r)
|
|
||||||
default:
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"encoding/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NotFound(w http.ResponseWriter, r *http.Request) {
|
|
||||||
json, _ := json.Marshal(map[string]string{
|
|
||||||
"status": "404",
|
|
||||||
"message": "Not Found",
|
|
||||||
"link": "http://docs.ely.by/skin-system.html",
|
|
||||||
})
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
w.Write(json)
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"net/http"
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/lib/data"
|
|
||||||
"elyby/minecraft-skinsystem/lib/tools"
|
|
||||||
"elyby/minecraft-skinsystem/lib/services"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SignedTextures(w http.ResponseWriter, r *http.Request) {
|
|
||||||
services.Logger.IncCounter("signed_textures.request", 1)
|
|
||||||
username := tools.ParseUsername(mux.Vars(r)["username"])
|
|
||||||
|
|
||||||
rec, err := data.FindSkinByUsername(username)
|
|
||||||
if (err != nil || rec.SkinId == 0 || rec.MojangTextures == "") {
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
responseData:= data.SignedTexturesResponse{
|
|
||||||
Id: strings.Replace(rec.Uuid, "-", "", -1),
|
|
||||||
Name: rec.Username,
|
|
||||||
Props: []data.Property{
|
|
||||||
{
|
|
||||||
Name: "textures",
|
|
||||||
Signature: rec.MojangSignature,
|
|
||||||
Value: rec.MojangTextures,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "ely",
|
|
||||||
Value: "but why are you asking?",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
response,_ := json.Marshal(responseData)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(response)
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/lib/tools"
|
|
||||||
"elyby/minecraft-skinsystem/lib/data"
|
|
||||||
"elyby/minecraft-skinsystem/lib/services"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Skin(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if (mux.Vars(r)["converted"] == "") {
|
|
||||||
services.Logger.IncCounter("skins.request", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
username := tools.ParseUsername(mux.Vars(r)["username"])
|
|
||||||
rec, err := data.FindSkinByUsername(username)
|
|
||||||
if (err != nil) {
|
|
||||||
http.Redirect(w, r, "http://skins.minecraft.net/MinecraftSkins/" + username + ".png", 301)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(w, r, tools.BuildElyUrl(rec.Url), 301);
|
|
||||||
}
|
|
||||||
|
|
||||||
func SkinGET(w http.ResponseWriter, r *http.Request) {
|
|
||||||
services.Logger.IncCounter("skins.get_request", 1)
|
|
||||||
username := r.URL.Query().Get("name")
|
|
||||||
if username == "" {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mux.Vars(r)["username"] = username
|
|
||||||
mux.Vars(r)["converted"] = "1"
|
|
||||||
Skin(w, r)
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/lib/data"
|
|
||||||
"elyby/minecraft-skinsystem/lib/tools"
|
|
||||||
"elyby/minecraft-skinsystem/lib/services"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Textures(w http.ResponseWriter, r *http.Request) {
|
|
||||||
services.Logger.IncCounter("textures.request", 1)
|
|
||||||
username := tools.ParseUsername(mux.Vars(r)["username"])
|
|
||||||
|
|
||||||
rec, err := data.FindSkinByUsername(username)
|
|
||||||
if (err != nil || rec.SkinId == 0) {
|
|
||||||
rec.Url = "http://skins.minecraft.net/MinecraftSkins/" + username + ".png"
|
|
||||||
rec.Hash = string(tools.BuildNonElyTexturesHash(username))
|
|
||||||
} else {
|
|
||||||
rec.Url = tools.BuildElyUrl(rec.Url)
|
|
||||||
}
|
|
||||||
|
|
||||||
textures := data.TexturesResponse{
|
|
||||||
Skin: &data.Skin{
|
|
||||||
Url: rec.Url,
|
|
||||||
Hash: rec.Hash,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rec.IsSlim) {
|
|
||||||
textures.Skin.Metadata = &data.SkinMetadata{
|
|
||||||
Model: "slim",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
capeRec, err := data.FindCapeByUsername(username)
|
|
||||||
if (err == nil) {
|
|
||||||
capeUrl, err := services.Router.Get("cloaks").URL("username", username)
|
|
||||||
if (err != nil) {
|
|
||||||
log.Println(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
var scheme string = "http://";
|
|
||||||
if (r.TLS != nil) {
|
|
||||||
scheme = "https://"
|
|
||||||
}
|
|
||||||
|
|
||||||
textures.Cape = &data.Cape{
|
|
||||||
Url: scheme + r.Host + capeUrl.String(),
|
|
||||||
Hash: capeRec.CalculateHash(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response,_ := json.Marshal(textures)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(response)
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package services
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/mediocregopher/radix.v2/pool"
|
|
||||||
"github.com/streadway/amqp"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/mono83/slf/wd"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Router *mux.Router
|
|
||||||
|
|
||||||
var RedisPool *pool.Pool
|
|
||||||
|
|
||||||
var RabbitMQChannel *amqp.Channel
|
|
||||||
|
|
||||||
var RootFolder string
|
|
||||||
|
|
||||||
var Logger wd.Watchdog
|
|
@ -1,45 +0,0 @@
|
|||||||
package tools
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"crypto/md5"
|
|
||||||
"strconv"
|
|
||||||
"encoding/hex"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ParseUsername(username string) string {
|
|
||||||
const suffix = ".png"
|
|
||||||
if strings.HasSuffix(username, suffix) {
|
|
||||||
username = strings.TrimSuffix(username, suffix)
|
|
||||||
}
|
|
||||||
|
|
||||||
return username
|
|
||||||
}
|
|
||||||
|
|
||||||
func BuildNonElyTexturesHash(username string) string {
|
|
||||||
hour := getCurrentHour()
|
|
||||||
hasher := md5.New()
|
|
||||||
hasher.Write([]byte("non-ely-" + strconv.FormatInt(hour, 10) + "-" + username))
|
|
||||||
|
|
||||||
return hex.EncodeToString(hasher.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func BuildKey(username string) string {
|
|
||||||
return "username:" + strings.ToLower(username)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BuildElyUrl(route string) string {
|
|
||||||
prefix := "http://ely.by"
|
|
||||||
if !strings.HasPrefix(route, prefix) {
|
|
||||||
route = prefix + route
|
|
||||||
}
|
|
||||||
|
|
||||||
return route
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCurrentHour() int64 {
|
|
||||||
n := time.Now()
|
|
||||||
return time.Date(n.Year(), n.Month(), n.Day(), n.Hour(), 0, 0, 0, time.UTC).Unix()
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
|||||||
package tools_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
. "elyby/minecraft-skinsystem/lib/tools"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseUsername(t *testing.T) {
|
|
||||||
if ParseUsername("test.png") != "test" {
|
|
||||||
t.Error("Function should trim .png at end")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ParseUsername("test") != "test" {
|
|
||||||
t.Error("Function should return string itself, if it not contains .png at end")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildKey(t *testing.T) {
|
|
||||||
if BuildKey("Test") != "username:test" {
|
|
||||||
t.Error("Function shound convert string to lower case and concatenate it with usernmae:")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildElyUrl(t *testing.T) {
|
|
||||||
if BuildElyUrl("/route") != "http://ely.by/route" {
|
|
||||||
t.Error("Function should add prefix to the provided relative url.")
|
|
||||||
}
|
|
||||||
|
|
||||||
if BuildElyUrl("http://ely.by/test/route") != "http://ely.by/test/route" {
|
|
||||||
t.Error("Function should do not add prefix to the provided prefixed url.")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
package tools
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"bytes"
|
|
||||||
"compress/zlib"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ZlibEncode(str []byte) []byte {
|
|
||||||
var buff bytes.Buffer
|
|
||||||
writer := zlib.NewWriter(&buff)
|
|
||||||
writer.Write(str)
|
|
||||||
writer.Close()
|
|
||||||
|
|
||||||
return buff.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func ZlibDecode(bts []byte) ([]byte, error) {
|
|
||||||
buff := bytes.NewReader(bts)
|
|
||||||
reader, readError := zlib.NewReader(buff)
|
|
||||||
if readError != nil {
|
|
||||||
return nil, readError
|
|
||||||
}
|
|
||||||
|
|
||||||
resultBuffer := new(bytes.Buffer)
|
|
||||||
io.Copy(resultBuffer, reader)
|
|
||||||
reader.Close()
|
|
||||||
|
|
||||||
return resultBuffer.Bytes(), nil
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
package worker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"elyby/minecraft-skinsystem/lib/data"
|
|
||||||
"elyby/minecraft-skinsystem/lib/services"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handleChangeUsername(model usernameChanged) (bool) {
|
|
||||||
if (model.OldUsername == "") {
|
|
||||||
services.Logger.IncCounter("worker.change_username.empty_old_username", 1)
|
|
||||||
record := data.SkinItem{
|
|
||||||
UserId: model.AccountId,
|
|
||||||
Username: model.NewUsername,
|
|
||||||
}
|
|
||||||
|
|
||||||
record.Save()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
record, err := data.FindSkinById(model.AccountId)
|
|
||||||
if (err != nil) {
|
|
||||||
services.Logger.IncCounter("worker.change_username.id_not_found", 1)
|
|
||||||
fmt.Println("Cannot find user id. Trying to search.")
|
|
||||||
response, err := getById(model.AccountId)
|
|
||||||
if err != nil {
|
|
||||||
services.Logger.IncCounter("worker.change_username.id_not_restored", 1)
|
|
||||||
fmt.Printf("Cannot restore user info. %T\n", err)
|
|
||||||
// TODO: логгировать в какой-нибудь Sentry, если там не 404
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
services.Logger.IncCounter("worker.change_username.id_restored", 1)
|
|
||||||
fmt.Println("User info successfully restored.")
|
|
||||||
record = data.SkinItem{
|
|
||||||
UserId: response.Id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
record.Username = model.NewUsername
|
|
||||||
record.Save()
|
|
||||||
|
|
||||||
services.Logger.IncCounter("worker.change_username.processed", 1)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSkinChanged(model skinChanged) bool {
|
|
||||||
record, err := data.FindSkinById(model.AccountId)
|
|
||||||
if err != nil {
|
|
||||||
services.Logger.IncCounter("worker.skin_changed.id_not_found", 1)
|
|
||||||
fmt.Println("Cannot find user id. Trying to search.")
|
|
||||||
response, err := getById(model.AccountId)
|
|
||||||
if err != nil {
|
|
||||||
services.Logger.IncCounter("worker.skin_changed.id_not_restored", 1)
|
|
||||||
fmt.Printf("Cannot restore user info. %T\n", err)
|
|
||||||
// TODO: логгировать в какой-нибудь Sentry, если там не 404
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
services.Logger.IncCounter("worker.skin_changed.id_restored", 1)
|
|
||||||
fmt.Println("User info successfully restored.")
|
|
||||||
record.UserId = response.Id
|
|
||||||
record.Username = response.Username
|
|
||||||
}
|
|
||||||
|
|
||||||
record.Uuid = model.Uuid
|
|
||||||
record.SkinId = model.SkinId
|
|
||||||
record.Hash = model.Hash
|
|
||||||
record.Is1_8 = model.Is1_8
|
|
||||||
record.IsSlim = model.IsSlim
|
|
||||||
record.Url = model.Url
|
|
||||||
record.MojangTextures = model.MojangTextures
|
|
||||||
record.MojangSignature = model.MojangSignature
|
|
||||||
|
|
||||||
record.Save()
|
|
||||||
|
|
||||||
services.Logger.IncCounter("worker.skin_changed.processed", 1)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
package worker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"elyby/minecraft-skinsystem/lib/external/accounts"
|
|
||||||
)
|
|
||||||
|
|
||||||
var AccountsTokenConfig *accounts.TokenRequest
|
|
||||||
|
|
||||||
var token *accounts.Token
|
|
||||||
|
|
||||||
const repeatsLimit = 3
|
|
||||||
var repeatsCount = 0
|
|
||||||
|
|
||||||
func getById(id int) (accounts.AccountInfoResponse, error) {
|
|
||||||
return _getByField("id", strconv.Itoa(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
func _getByField(field string, value string) (accounts.AccountInfoResponse, error) {
|
|
||||||
defer resetRepeatsCount()
|
|
||||||
|
|
||||||
apiToken, err := getToken()
|
|
||||||
if err != nil {
|
|
||||||
return accounts.AccountInfoResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := apiToken.AccountInfo(field, value)
|
|
||||||
if err != nil {
|
|
||||||
_, ok := err.(*accounts.UnauthorizedResponse)
|
|
||||||
if !ok || repeatsCount >= repeatsLimit {
|
|
||||||
return accounts.AccountInfoResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
repeatsCount++
|
|
||||||
token = nil
|
|
||||||
|
|
||||||
return _getByField(field, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getToken() (*accounts.Token, error) {
|
|
||||||
if token == nil {
|
|
||||||
println("token is nil, trying to obtain new one")
|
|
||||||
tempToken, err := accounts.GetToken(*AccountsTokenConfig)
|
|
||||||
if err != nil {
|
|
||||||
println("cannot obtain new one token", err)
|
|
||||||
return &accounts.Token{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
token = &tempToken
|
|
||||||
}
|
|
||||||
|
|
||||||
return token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func resetRepeatsCount() {
|
|
||||||
repeatsCount = 0
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
package worker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/lib/services"
|
|
||||||
)
|
|
||||||
|
|
||||||
const exchangeName string = "events"
|
|
||||||
const queueName string = "skinsystem-accounts-events"
|
|
||||||
|
|
||||||
func Listen() {
|
|
||||||
var err error
|
|
||||||
ch := services.RabbitMQChannel
|
|
||||||
|
|
||||||
err = ch.ExchangeDeclare(
|
|
||||||
exchangeName, // name
|
|
||||||
"topic", // type
|
|
||||||
true, // durable
|
|
||||||
false, // auto-deleted
|
|
||||||
false, // internal
|
|
||||||
false, // no-wait
|
|
||||||
nil, // arguments
|
|
||||||
)
|
|
||||||
failOnError(err, "Failed to declare an exchange")
|
|
||||||
|
|
||||||
_, err = ch.QueueDeclare(
|
|
||||||
queueName, // name
|
|
||||||
true, // durable
|
|
||||||
false, // delete when usused
|
|
||||||
false, // exclusive
|
|
||||||
false, // no-wait
|
|
||||||
nil, // arguments
|
|
||||||
)
|
|
||||||
failOnError(err, "Failed to declare a queue")
|
|
||||||
|
|
||||||
err = ch.QueueBind(queueName, "accounts.username-changed", exchangeName, false, nil)
|
|
||||||
failOnError(err, "Failed to bind a queue")
|
|
||||||
|
|
||||||
err = ch.QueueBind(queueName, "accounts.skin-changed", exchangeName, false, nil)
|
|
||||||
failOnError(err, "Failed to bind a queue")
|
|
||||||
|
|
||||||
msgs, err := ch.Consume(
|
|
||||||
queueName, // queue
|
|
||||||
"", // consumer
|
|
||||||
false, // auto-ack
|
|
||||||
false, // exclusive
|
|
||||||
false, // no-local
|
|
||||||
false, // no-wait
|
|
||||||
nil, // args
|
|
||||||
)
|
|
||||||
failOnError(err, "Failed to register a consumer")
|
|
||||||
|
|
||||||
forever := make(chan bool)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for d := range msgs {
|
|
||||||
log.Println("Incoming message with routing key " + d.RoutingKey)
|
|
||||||
var result bool = true;
|
|
||||||
switch d.RoutingKey {
|
|
||||||
case "accounts.username-changed":
|
|
||||||
var model usernameChanged
|
|
||||||
json.Unmarshal(d.Body, &model)
|
|
||||||
result = handleChangeUsername(model)
|
|
||||||
case "accounts.skin-changed":
|
|
||||||
var model skinChanged
|
|
||||||
json.Unmarshal(d.Body, &model)
|
|
||||||
result = handleSkinChanged(model)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
d.Ack(false)
|
|
||||||
} else {
|
|
||||||
d.Reject(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
<-forever
|
|
||||||
}
|
|
||||||
|
|
||||||
func failOnError(err error, msg string) {
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("%s: %s", msg, err)
|
|
||||||
}
|
|
||||||
}
|
|
132
logger/receivers/sentry/receiver.go
Normal file
132
logger/receivers/sentry/receiver.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package sentry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/getsentry/raven-go"
|
||||||
|
"github.com/mono83/slf"
|
||||||
|
"github.com/mono83/slf/filters"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds information for filtered receiver
|
||||||
|
type Config struct {
|
||||||
|
MinLevel string
|
||||||
|
ParamsWhiteList []string
|
||||||
|
ParamsBlackList []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReceiver allows you to create a new receiver in the Sentry
|
||||||
|
// using the fastest and easiest way.
|
||||||
|
// The Config parameter can be passed as nil if you do not need additional filtration.
|
||||||
|
func NewReceiver(dsn string, cfg *Config) (slf.Receiver, error) {
|
||||||
|
client, err := raven.New(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewReceiverWithCustomRaven(client, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReceiverWithCustomRaven allows you to create a new receiver in the Sentry
|
||||||
|
// configuring raven.Client by yourself. This can be useful if you need to set
|
||||||
|
// additional parameters, such as release and environment, that will be sent
|
||||||
|
// with each Packet in the Sentry:
|
||||||
|
//
|
||||||
|
// client, err := raven.New("https://some:sentry@dsn.sentry.io/1")
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// client.SetRelease("1.3.2")
|
||||||
|
// client.SetEnvironment("production")
|
||||||
|
// client.SetDefaultLoggerName("sentry-watchdog-receiver")
|
||||||
|
//
|
||||||
|
// sentryReceiver, err := sentry.NewReceiverWithCustomRaven(client, &sentry.Config{
|
||||||
|
// MinLevel: "warn",
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// The Config parameter allows you to add additional filtering, such as the minimum
|
||||||
|
// message level and the exclusion of private parameters. If you do not need additional
|
||||||
|
// filtering, nil can passed.
|
||||||
|
func NewReceiverWithCustomRaven(client *raven.Client, cfg *Config) (slf.Receiver, error) {
|
||||||
|
out, err := buildReceiverForClient(client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg == nil {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolving level
|
||||||
|
level, ok := slf.ParseType(cfg.MinLevel)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("Unknown level %s", cfg.MinLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.ParamsWhiteList) > 0 {
|
||||||
|
out.filter = slf.NewWhiteListParamsFilter(cfg.ParamsWhiteList)
|
||||||
|
} else {
|
||||||
|
out.filter = slf.NewBlackListParamsFilter(cfg.ParamsBlackList)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters.MinLogLevel(level, out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildReceiverForClient(client *raven.Client) (*sentryLogReceiver, error) {
|
||||||
|
return &sentryLogReceiver{target: client, filter: slf.NewBlackListParamsFilter(nil)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type sentryLogReceiver struct {
|
||||||
|
target *raven.Client
|
||||||
|
filter slf.ParamsFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l sentryLogReceiver) Receive(p slf.Event) {
|
||||||
|
if !p.IsLog() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt := raven.NewPacket(
|
||||||
|
slf.ReplacePlaceholders(p.Content, p.Params, false),
|
||||||
|
// First 5 means, that first N elements will be skipped before actual app trace
|
||||||
|
// This is needed to exclude watchdog calls from stack trace
|
||||||
|
raven.NewStacktrace(5, 5, []string{}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(p.Params) > 0 {
|
||||||
|
shownParams := l.filter(p.Params)
|
||||||
|
for _, param := range shownParams {
|
||||||
|
value := param.GetRaw()
|
||||||
|
if e, ok := value.(error); ok && e != nil {
|
||||||
|
value = e.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt.Extra[param.GetKey()] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt.Level = convertType(p.Type)
|
||||||
|
pkt.Timestamp = raven.Timestamp(p.Time)
|
||||||
|
|
||||||
|
l.target.Capture(pkt, map[string]string{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertType(wdType byte) raven.Severity {
|
||||||
|
switch wdType {
|
||||||
|
case slf.TypeTrace:
|
||||||
|
case slf.TypeDebug:
|
||||||
|
return raven.DEBUG
|
||||||
|
case slf.TypeInfo:
|
||||||
|
return raven.INFO
|
||||||
|
case slf.TypeWarning:
|
||||||
|
return raven.WARNING
|
||||||
|
case slf.TypeError:
|
||||||
|
return raven.ERROR
|
||||||
|
case slf.TypeAlert:
|
||||||
|
case slf.TypeEmergency:
|
||||||
|
return raven.FATAL
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("Unknown wd type " + string(wdType))
|
||||||
|
}
|
12
main.go
Normal file
12
main.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
@ -1,147 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"log"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/streadway/amqp"
|
|
||||||
"github.com/mediocregopher/radix.v2/pool"
|
|
||||||
"github.com/mono83/slf/wd"
|
|
||||||
"github.com/mono83/slf/rays"
|
|
||||||
"github.com/mono83/slf/recievers/ansi"
|
|
||||||
"github.com/mono83/slf/recievers/statsd"
|
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/lib/routes"
|
|
||||||
"elyby/minecraft-skinsystem/lib/services"
|
|
||||||
"elyby/minecraft-skinsystem/lib/worker"
|
|
||||||
"elyby/minecraft-skinsystem/lib/external/accounts"
|
|
||||||
)
|
|
||||||
|
|
||||||
const redisPoolSize int = 10
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.Println("Starting...")
|
|
||||||
|
|
||||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
|
||||||
|
|
||||||
accountsApiId := os.Getenv("ACCOUNTS_API_ID")
|
|
||||||
accountsApiSecret := os.Getenv("ACCOUNTS_API_SECRET")
|
|
||||||
if accountsApiId == "" || accountsApiSecret == "" {
|
|
||||||
log.Fatal("ACCOUNTS_API params must be provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
worker.AccountsTokenConfig = &accounts.TokenRequest{
|
|
||||||
Id: accountsApiId,
|
|
||||||
Secret: accountsApiSecret,
|
|
||||||
Scopes: []string{
|
|
||||||
"internal_account_info",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Connecting to redis")
|
|
||||||
|
|
||||||
var redisString = os.Getenv("REDIS_ADDR")
|
|
||||||
if (redisString == "") {
|
|
||||||
redisString = "redis:6379"
|
|
||||||
}
|
|
||||||
|
|
||||||
redisPool, redisErr := pool.New("tcp", redisString, redisPoolSize)
|
|
||||||
if (redisErr != nil) {
|
|
||||||
log.Fatal("Redis unavailable")
|
|
||||||
}
|
|
||||||
log.Println("Connected to redis")
|
|
||||||
|
|
||||||
log.Println("Connecting to rabbitmq")
|
|
||||||
// TODO: rabbitmq становится доступен не сразу. Нужно дождаться, пока он станет доступен, периодически повторяя запросы
|
|
||||||
|
|
||||||
var rabbitmqString = os.Getenv("RABBITMQ_ADDR")
|
|
||||||
if (rabbitmqString == "") {
|
|
||||||
rabbitmqString = "amqp://ely-skinsystem-app:ely-skinsystem-app-password@rabbitmq:5672/%2fely"
|
|
||||||
}
|
|
||||||
|
|
||||||
rabbitConnection, rabbitmqErr := amqp.Dial(rabbitmqString)
|
|
||||||
if (rabbitmqErr != nil) {
|
|
||||||
log.Fatalf("%s", rabbitmqErr)
|
|
||||||
}
|
|
||||||
log.Println("Connected to rabbitmq. Trying to open a channel")
|
|
||||||
rabbitChannel, rabbitmqErr := rabbitConnection.Channel()
|
|
||||||
if (rabbitmqErr != nil) {
|
|
||||||
log.Fatalf("%s", rabbitmqErr)
|
|
||||||
}
|
|
||||||
log.Println("Connected to rabbitmq channel")
|
|
||||||
|
|
||||||
// statsd
|
|
||||||
var statsdString = os.Getenv("STATSD_ADDR")
|
|
||||||
if (statsdString != "") {
|
|
||||||
log.Println("Connecting to statsd")
|
|
||||||
hostname, _ := os.Hostname()
|
|
||||||
statsdReceiver, err := statsd.NewReceiver(statsd.Config{
|
|
||||||
Address: statsdString,
|
|
||||||
Prefix: "ely.skinsystem." + hostname + ".app.",
|
|
||||||
FlushEvery: 1,
|
|
||||||
})
|
|
||||||
if (err != nil) {
|
|
||||||
log.Fatal("statsd connection error")
|
|
||||||
}
|
|
||||||
|
|
||||||
wd.AddReceiver(statsdReceiver)
|
|
||||||
} else {
|
|
||||||
wd.AddReceiver(ansi.New(true, true, false))
|
|
||||||
}
|
|
||||||
|
|
||||||
logger := wd.New("", "").WithParams(rays.Host)
|
|
||||||
|
|
||||||
router := mux.NewRouter().StrictSlash(true)
|
|
||||||
router.HandleFunc("/skins/{username}", routes.Skin).Methods("GET").Name("skins")
|
|
||||||
router.HandleFunc("/cloaks/{username}", routes.Cape).Methods("GET").Name("cloaks")
|
|
||||||
router.HandleFunc("/textures/{username}", routes.Textures).Methods("GET").Name("textures")
|
|
||||||
router.HandleFunc("/textures/signed/{username}", routes.SignedTextures).Methods("GET").Name("signedTextures")
|
|
||||||
router.HandleFunc("/skins/{username}/face", routes.Face).Methods("GET").Name("faces")
|
|
||||||
router.HandleFunc("/skins/{username}/face.png", routes.Face).Methods("GET").Name("faces")
|
|
||||||
// Legacy
|
|
||||||
router.HandleFunc("/minecraft.php", routes.MinecraftPHP).Methods("GET")
|
|
||||||
router.HandleFunc("/skins/", routes.SkinGET).Methods("GET")
|
|
||||||
router.HandleFunc("/cloaks/", routes.CapeGET).Methods("GET")
|
|
||||||
// 404
|
|
||||||
router.NotFoundHandler = http.HandlerFunc(routes.NotFound)
|
|
||||||
|
|
||||||
services.Router = router
|
|
||||||
services.RedisPool = redisPool
|
|
||||||
services.RabbitMQChannel = rabbitChannel
|
|
||||||
services.Logger = logger
|
|
||||||
|
|
||||||
_, file, _, _ := runtime.Caller(0)
|
|
||||||
services.RootFolder = filepath.Dir(file)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
period := 5
|
|
||||||
for {
|
|
||||||
time.Sleep(time.Duration(period) * time.Second)
|
|
||||||
|
|
||||||
resp := services.RedisPool.Cmd("PING")
|
|
||||||
if (resp.Err == nil) {
|
|
||||||
// Если редис успешно пинганулся, значит всё хорошо
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Redis not pinged. Try to reconnect")
|
|
||||||
newPool, redisErr := pool.New("tcp", redisString, redisPoolSize)
|
|
||||||
if (redisErr != nil) {
|
|
||||||
log.Printf("Cannot reconnect to redis, waiting %d seconds\n", period)
|
|
||||||
} else {
|
|
||||||
services.RedisPool = newPool
|
|
||||||
log.Println("Reconnected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go worker.Listen()
|
|
||||||
|
|
||||||
log.Println("Started");
|
|
||||||
log.Fatal(http.ListenAndServe(":80", router))
|
|
||||||
}
|
|
9
model/cape.go
Normal file
9
model/cape.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cape struct {
|
||||||
|
File io.Reader
|
||||||
|
}
|
@ -1,20 +1,15 @@
|
|||||||
package worker
|
package model
|
||||||
|
|
||||||
type usernameChanged struct {
|
type Skin struct {
|
||||||
AccountId int `json:"accountId"`
|
UserId int `json:"userId"`
|
||||||
OldUsername string `json:"oldUsername"`
|
|
||||||
NewUsername string `json:"newUsername"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type skinChanged struct {
|
|
||||||
AccountId int `json:"userId"`
|
|
||||||
Uuid string `json:"uuid"`
|
Uuid string `json:"uuid"`
|
||||||
|
Username string `json:"username"`
|
||||||
SkinId int `json:"skinId"`
|
SkinId int `json:"skinId"`
|
||||||
OldSkinId int `json:"oldSkinId"`
|
Url string `json:"url"`
|
||||||
Hash string `json:"hash"`
|
|
||||||
Is1_8 bool `json:"is1_8"`
|
Is1_8 bool `json:"is1_8"`
|
||||||
IsSlim bool `json:"isSlim"`
|
IsSlim bool `json:"isSlim"`
|
||||||
Url string `json:"url"`
|
Hash string `json:"hash"`
|
||||||
MojangTextures string `json:"mojangTextures"`
|
MojangTextures string `json:"mojangTextures"`
|
||||||
MojangSignature string `json:"mojangSignature"`
|
MojangSignature string `json:"mojangSignature"`
|
||||||
|
OldUsername string
|
||||||
}
|
}
|
46
script/coverage
Executable file
46
script/coverage
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Based on https://github.com/mlafeldt/chef-runner/blob/34269dbb726c243dff9764007e7bd7f0fe9ee331/script/coverage
|
||||||
|
# Generate test coverage statistics for Go packages.
|
||||||
|
#
|
||||||
|
# Works around the fact that `go test -coverprofile` currently does not work
|
||||||
|
# with multiple packages, see https://code.google.com/p/go/issues/detail?id=6909
|
||||||
|
#
|
||||||
|
# Usage: script/coverage [--html]
|
||||||
|
#
|
||||||
|
# --html Additionally create HTML report and open it in browser
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
workdir=.cover
|
||||||
|
profile="$workdir/cover.out"
|
||||||
|
mode=count
|
||||||
|
|
||||||
|
generate_cover_data() {
|
||||||
|
rm -rf "$workdir"
|
||||||
|
mkdir "$workdir"
|
||||||
|
|
||||||
|
go test -i "$@" # compile dependencies first before serializing go test invocations
|
||||||
|
for pkg in "$@"; do
|
||||||
|
f="$workdir/$(echo $pkg | tr / -).cover"
|
||||||
|
go test -covermode="$mode" -coverprofile="$f" "$pkg"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "mode: $mode" >"$profile"
|
||||||
|
grep -h -v "^mode:" "$workdir"/*.cover >>"$profile"
|
||||||
|
}
|
||||||
|
|
||||||
|
show_cover_report() {
|
||||||
|
go tool cover -${1}="$profile"
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_cover_data $(go list ./... | grep -v /vendor/)
|
||||||
|
show_cover_report func
|
||||||
|
case "$1" in
|
||||||
|
"")
|
||||||
|
;;
|
||||||
|
--html)
|
||||||
|
show_cover_report html ;;
|
||||||
|
*)
|
||||||
|
echo >&2 "error: invalid option: $1"; exit 1 ;;
|
||||||
|
esac
|
27
script/test
Executable file
27
script/test
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Based on https://github.com/mlafeldt/chef-runner/blob/34269dbb726c243dff9764007e7bd7f0fe9ee331/script/test
|
||||||
|
# Run package tests for a file/directory, or all tests if no argument is passed.
|
||||||
|
# Useful to e.g. execute package tests for the file currently open in Vim.
|
||||||
|
# Usage: script/test [path]
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
go_pkg_from_path() {
|
||||||
|
path=$1
|
||||||
|
if test -d "$path"; then
|
||||||
|
dir="$path"
|
||||||
|
else
|
||||||
|
dir=$(dirname "$path")
|
||||||
|
fi
|
||||||
|
(cd "$dir" && go list)
|
||||||
|
}
|
||||||
|
|
||||||
|
if test $# -gt 0; then
|
||||||
|
pkg=$(go_pkg_from_path "$1")
|
||||||
|
verbose=-v
|
||||||
|
else
|
||||||
|
pkg=$(go list ./... | grep -v /vendor/)
|
||||||
|
verbose=
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec go test ${GOTESTOPTS:-$verbose} $pkg
|
187
worker/worder_test.go
Normal file
187
worker/worder_test.go
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
package worker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/api/accounts"
|
||||||
|
"elyby/minecraft-skinsystem/db"
|
||||||
|
"elyby/minecraft-skinsystem/interfaces/mock_interfaces"
|
||||||
|
"elyby/minecraft-skinsystem/interfaces/mock_wd"
|
||||||
|
"elyby/minecraft-skinsystem/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServices_HandleChangeUsername(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
services, skinRepo, _, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
resultModel := createSourceModel()
|
||||||
|
resultModel.Username = "new_username"
|
||||||
|
|
||||||
|
// Запись о скине существует, никаких осложнений
|
||||||
|
skinRepo.EXPECT().FindByUserId(1).Return(createSourceModel(), nil)
|
||||||
|
skinRepo.EXPECT().Save(resultModel)
|
||||||
|
wd.EXPECT().IncCounter("worker.change_username", int64(1))
|
||||||
|
|
||||||
|
assert.True(services.HandleChangeUsername(&UsernameChanged{
|
||||||
|
AccountId: 1,
|
||||||
|
OldUsername: "mock_user",
|
||||||
|
NewUsername: "new_username",
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Событие с пустым ником, т.е это регистрация, так что нужно создать запись о скине
|
||||||
|
skinRepo.EXPECT().FindByUserId(1).Times(0)
|
||||||
|
skinRepo.EXPECT().Save(&model.Skin{UserId: 1, Username: "new_mock"})
|
||||||
|
wd.EXPECT().IncCounter("worker.change_username", int64(1))
|
||||||
|
wd.EXPECT().IncCounter("worker.change_username_empty_old_username", int64(1))
|
||||||
|
|
||||||
|
assert.True(services.HandleChangeUsername(&UsernameChanged{
|
||||||
|
AccountId: 1,
|
||||||
|
OldUsername: "",
|
||||||
|
NewUsername: "new_mock",
|
||||||
|
}))
|
||||||
|
|
||||||
|
// В базе системы скинов нет записи об указанном пользователе, так что её нужно восстановить
|
||||||
|
skinRepo.EXPECT().FindByUserId(1).Return(nil, &db.SkinNotFoundError{})
|
||||||
|
skinRepo.EXPECT().Save(&model.Skin{UserId: 1, Username: "new_mock2"})
|
||||||
|
wd.EXPECT().IncCounter("worker.change_username", int64(1))
|
||||||
|
wd.EXPECT().IncCounter("worker.change_username_id_not_found", int64(1))
|
||||||
|
wd.EXPECT().Info("Cannot find user id :accountId. Trying to search.", gomock.Any())
|
||||||
|
|
||||||
|
assert.True(services.HandleChangeUsername(&UsernameChanged{
|
||||||
|
AccountId: 1,
|
||||||
|
OldUsername: "mock_user",
|
||||||
|
NewUsername: "new_mock2",
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Репозиторий вернул неожиданную ошибку
|
||||||
|
skinRepo.EXPECT().FindByUserId(1).Return(nil, errors.New("mock error"))
|
||||||
|
skinRepo.EXPECT().Save(&model.Skin{UserId: 1, Username: "new_mock2"})
|
||||||
|
wd.EXPECT().IncCounter("worker.change_username", int64(1))
|
||||||
|
wd.EXPECT().IncCounter("worker.change_username_id_not_found", int64(1))
|
||||||
|
wd.EXPECT().Info("Cannot find user id :accountId. Trying to search.", gomock.Any())
|
||||||
|
wd.EXPECT().Error("Unknown error when requesting a skin from the repository: :err", gomock.Any())
|
||||||
|
|
||||||
|
assert.True(services.HandleChangeUsername(&UsernameChanged{
|
||||||
|
AccountId: 1,
|
||||||
|
OldUsername: "mock_user",
|
||||||
|
NewUsername: "new_mock2",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServices_HandleSkinChanged(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
services, skinRepo, accountsAPI, wd := setupMocks(ctrl)
|
||||||
|
|
||||||
|
event := &SkinChanged{
|
||||||
|
AccountId: 1,
|
||||||
|
Uuid: "cdb907ce-84f4-4c38-801d-1e287dca2623",
|
||||||
|
SkinId: 2,
|
||||||
|
OldSkinId: 1,
|
||||||
|
Hash: "f76caa016e07267a05b7daf9ebc7419c",
|
||||||
|
Is1_8: true,
|
||||||
|
IsSlim: false,
|
||||||
|
Url: "http://ely.by/minecraft/skins/69c6740d2993e5d6f6a7fc92420efc29.png",
|
||||||
|
MojangTextures: "new mocked textures base64",
|
||||||
|
MojangSignature: "new mocked signature",
|
||||||
|
}
|
||||||
|
|
||||||
|
resultModel := createSourceModel()
|
||||||
|
resultModel.SkinId = event.SkinId
|
||||||
|
resultModel.Hash = event.Hash
|
||||||
|
resultModel.Is1_8 = event.Is1_8
|
||||||
|
resultModel.IsSlim = event.IsSlim
|
||||||
|
resultModel.Url = event.Url
|
||||||
|
resultModel.MojangTextures = event.MojangTextures
|
||||||
|
resultModel.MojangSignature = event.MojangSignature
|
||||||
|
|
||||||
|
// Запись о скине существует, никаких осложнений
|
||||||
|
skinRepo.EXPECT().FindByUserId(1).Return(createSourceModel(), nil)
|
||||||
|
skinRepo.EXPECT().Save(resultModel)
|
||||||
|
wd.EXPECT().IncCounter("worker.skin_changed", int64(1))
|
||||||
|
|
||||||
|
assert.True(services.HandleSkinChanged(event))
|
||||||
|
|
||||||
|
// Записи о скине не существует, она должна быть восстановлена
|
||||||
|
skinRepo.EXPECT().FindByUserId(1).Return(nil, &db.SkinNotFoundError{"mock_user"})
|
||||||
|
skinRepo.EXPECT().Save(resultModel)
|
||||||
|
accountsAPI.EXPECT().AccountInfo("id", "1").Return(&accounts.AccountInfoResponse{
|
||||||
|
Id: 1,
|
||||||
|
Username: "mock_user",
|
||||||
|
Uuid: "cdb907ce-84f4-4c38-801d-1e287dca2623",
|
||||||
|
Email: "mock-user@ely.by",
|
||||||
|
}, nil)
|
||||||
|
wd.EXPECT().IncCounter("worker.skin_changed", int64(1))
|
||||||
|
wd.EXPECT().IncCounter("worker.skin_changed_id_not_found", int64(1))
|
||||||
|
wd.EXPECT().IncCounter("worker.skin_changed_id_restored", int64(1))
|
||||||
|
wd.EXPECT().Info("Cannot find user id :accountId. Trying to search.", gomock.Any())
|
||||||
|
wd.EXPECT().Info("User info successfully restored.")
|
||||||
|
|
||||||
|
assert.True(services.HandleSkinChanged(event))
|
||||||
|
|
||||||
|
// Записи о скине не существует, и Ely.by Accounts internal API не знает о таком пользователе
|
||||||
|
skinRepo.EXPECT().FindByUserId(1).Return(nil, &db.SkinNotFoundError{"mock_user"})
|
||||||
|
accountsAPI.EXPECT().AccountInfo("id", "1").Return(nil, &accounts.NotFoundResponse{})
|
||||||
|
wd.EXPECT().IncCounter("worker.skin_changed", int64(1))
|
||||||
|
wd.EXPECT().IncCounter("worker.skin_changed_id_not_found", int64(1))
|
||||||
|
wd.EXPECT().IncCounter("worker.skin_changed_id_not_restored", int64(1))
|
||||||
|
wd.EXPECT().Info("Cannot find user id :accountId. Trying to search.", gomock.Any())
|
||||||
|
wd.EXPECT().Error("Cannot restore user info for :accountId: :err", gomock.Any(), gomock.Any())
|
||||||
|
|
||||||
|
assert.True(services.HandleSkinChanged(event))
|
||||||
|
|
||||||
|
// Репозиторий скинов вернул неизвестную ошибку, и Ely.by Accounts internal API не знает о таком пользователе
|
||||||
|
skinRepo.EXPECT().FindByUserId(1).Return(nil, errors.New("mocked error"))
|
||||||
|
accountsAPI.EXPECT().AccountInfo("id", "1").Return(nil, &accounts.NotFoundResponse{})
|
||||||
|
wd.EXPECT().IncCounter("worker.skin_changed", int64(1))
|
||||||
|
wd.EXPECT().IncCounter("worker.skin_changed_id_not_found", int64(1))
|
||||||
|
wd.EXPECT().IncCounter("worker.skin_changed_id_not_restored", int64(1))
|
||||||
|
wd.EXPECT().Error("Unknown error when requesting a skin from the repository: :err", gomock.Any())
|
||||||
|
wd.EXPECT().Info("Cannot find user id :accountId. Trying to search.", gomock.Any())
|
||||||
|
wd.EXPECT().Error("Cannot restore user info for :accountId: :err", gomock.Any(), gomock.Any())
|
||||||
|
|
||||||
|
assert.True(services.HandleSkinChanged(event))
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSourceModel() *model.Skin {
|
||||||
|
return &model.Skin{
|
||||||
|
UserId: 1,
|
||||||
|
Uuid: "cdb907ce-84f4-4c38-801d-1e287dca2623",
|
||||||
|
Username: "mock_user",
|
||||||
|
SkinId: 1,
|
||||||
|
Url: "http://ely.by/minecraft/skins/3a345c701f473ac08c8c5b8ecb58ecf3.png",
|
||||||
|
Is1_8: false,
|
||||||
|
IsSlim: false,
|
||||||
|
Hash: "3a345c701f473ac08c8c5b8ecb58ecf3",
|
||||||
|
MojangTextures: "mocked textures base64",
|
||||||
|
MojangSignature: "mocked signature",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupMocks(ctrl *gomock.Controller) (
|
||||||
|
*Services,
|
||||||
|
*mock_interfaces.MockSkinsRepository,
|
||||||
|
*mock_interfaces.MockAccountsAPI,
|
||||||
|
*mock_wd.MockWatchdog,
|
||||||
|
) {
|
||||||
|
skinsRepo := mock_interfaces.NewMockSkinsRepository(ctrl)
|
||||||
|
accountApi := mock_interfaces.NewMockAccountsAPI(ctrl)
|
||||||
|
wd := mock_wd.NewMockWatchdog(ctrl)
|
||||||
|
|
||||||
|
return &Services{
|
||||||
|
SkinsRepo: skinsRepo,
|
||||||
|
AccountsAPI: accountApi,
|
||||||
|
Logger: wd,
|
||||||
|
}, skinsRepo, accountApi, wd
|
||||||
|
}
|
220
worker/worker.go
Normal file
220
worker/worker.go
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
package worker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/assembla/cony"
|
||||||
|
"github.com/mono83/slf/wd"
|
||||||
|
"github.com/streadway/amqp"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/db"
|
||||||
|
"elyby/minecraft-skinsystem/interfaces"
|
||||||
|
"elyby/minecraft-skinsystem/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Services struct {
|
||||||
|
AmqpClient *cony.Client
|
||||||
|
SkinsRepo interfaces.SkinsRepository
|
||||||
|
AccountsAPI interfaces.AccountsAPI
|
||||||
|
Logger wd.Watchdog
|
||||||
|
}
|
||||||
|
|
||||||
|
type UsernameChanged struct {
|
||||||
|
AccountId int `json:"accountId"`
|
||||||
|
OldUsername string `json:"oldUsername"`
|
||||||
|
NewUsername string `json:"newUsername"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SkinChanged struct {
|
||||||
|
AccountId int `json:"userId"`
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
SkinId int `json:"skinId"`
|
||||||
|
OldSkinId int `json:"oldSkinId"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Is1_8 bool `json:"is1_8"`
|
||||||
|
IsSlim bool `json:"isSlim"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
MojangTextures string `json:"mojangTextures"`
|
||||||
|
MojangSignature string `json:"mojangSignature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const exchangeName string = "events"
|
||||||
|
const queueName string = "skinsystem-accounts-events"
|
||||||
|
|
||||||
|
func (service *Services) Run() error {
|
||||||
|
clientErrs, consumerErrs, deliveryChannel := setupClient(service.AmqpClient)
|
||||||
|
shouldReturnError := true
|
||||||
|
|
||||||
|
for service.AmqpClient.Loop() {
|
||||||
|
select {
|
||||||
|
case msg := <-deliveryChannel:
|
||||||
|
shouldReturnError = false
|
||||||
|
service.HandleDelivery(&msg)
|
||||||
|
case err := <-consumerErrs:
|
||||||
|
if shouldReturnError {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Logger.Error("Consume error: :err", wd.ErrParam(err))
|
||||||
|
case err := <-clientErrs:
|
||||||
|
if shouldReturnError {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Logger.Error("Client error: :err", wd.ErrParam(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Services) HandleDelivery(delivery *amqp.Delivery) {
|
||||||
|
service.Logger.Debug("Incoming message with routing key " + delivery.RoutingKey)
|
||||||
|
var result bool = true
|
||||||
|
switch delivery.RoutingKey {
|
||||||
|
case "accounts.username-changed":
|
||||||
|
var event *UsernameChanged
|
||||||
|
json.Unmarshal(delivery.Body, &event)
|
||||||
|
result = service.HandleChangeUsername(event)
|
||||||
|
case "accounts.skin-changed":
|
||||||
|
var event *SkinChanged
|
||||||
|
json.Unmarshal(delivery.Body, &event)
|
||||||
|
result = service.HandleSkinChanged(event)
|
||||||
|
default:
|
||||||
|
service.Logger.Info("Unknown delivery with routing key " + delivery.RoutingKey)
|
||||||
|
delivery.Ack(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if result {
|
||||||
|
delivery.Ack(false)
|
||||||
|
} else {
|
||||||
|
delivery.Reject(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Services) HandleChangeUsername(event *UsernameChanged) bool {
|
||||||
|
service.Logger.IncCounter("worker.change_username", 1)
|
||||||
|
if event.OldUsername == "" {
|
||||||
|
service.Logger.IncCounter("worker.change_username_empty_old_username", 1)
|
||||||
|
record := &model.Skin{
|
||||||
|
UserId: event.AccountId,
|
||||||
|
Username: event.NewUsername,
|
||||||
|
}
|
||||||
|
|
||||||
|
service.SkinsRepo.Save(record)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err := service.SkinsRepo.FindByUserId(event.AccountId)
|
||||||
|
if err != nil {
|
||||||
|
service.Logger.Info("Cannot find user id :accountId. Trying to search.", wd.IntParam("accountId", event.AccountId))
|
||||||
|
if _, isSkinNotFound := err.(*db.SkinNotFoundError); !isSkinNotFound {
|
||||||
|
service.Logger.Error("Unknown error when requesting a skin from the repository: :err", wd.ErrParam(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Logger.IncCounter("worker.change_username_id_not_found", 1)
|
||||||
|
record = &model.Skin{
|
||||||
|
UserId: event.AccountId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Username = event.NewUsername
|
||||||
|
service.SkinsRepo.Save(record)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: возможно стоит добавить проверку на совпадение id аккаунтов
|
||||||
|
func (service *Services) HandleSkinChanged(event *SkinChanged) bool {
|
||||||
|
service.Logger.IncCounter("worker.skin_changed", 1)
|
||||||
|
var record *model.Skin
|
||||||
|
record, err := service.SkinsRepo.FindByUserId(event.AccountId)
|
||||||
|
if err != nil {
|
||||||
|
if _, isSkinNotFound := err.(*db.SkinNotFoundError); !isSkinNotFound {
|
||||||
|
service.Logger.Error("Unknown error when requesting a skin from the repository: :err", wd.ErrParam(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Logger.IncCounter("worker.skin_changed_id_not_found", 1)
|
||||||
|
service.Logger.Info("Cannot find user id :accountId. Trying to search.", wd.IntParam("accountId", event.AccountId))
|
||||||
|
response, err := service.AccountsAPI.AccountInfo("id", strconv.Itoa(event.AccountId))
|
||||||
|
if err != nil {
|
||||||
|
service.Logger.IncCounter("worker.skin_changed_id_not_restored", 1)
|
||||||
|
service.Logger.Error(
|
||||||
|
"Cannot restore user info for :accountId: :err",
|
||||||
|
wd.IntParam("accountId", event.AccountId),
|
||||||
|
wd.ErrParam(err),
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Logger.IncCounter("worker.skin_changed_id_restored", 1)
|
||||||
|
service.Logger.Info("User info successfully restored.")
|
||||||
|
|
||||||
|
record = &model.Skin{
|
||||||
|
UserId: response.Id,
|
||||||
|
Username: response.Username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Uuid = event.Uuid
|
||||||
|
record.SkinId = event.SkinId
|
||||||
|
record.Hash = event.Hash
|
||||||
|
record.Is1_8 = event.Is1_8
|
||||||
|
record.IsSlim = event.IsSlim
|
||||||
|
record.Url = event.Url
|
||||||
|
record.MojangTextures = event.MojangTextures
|
||||||
|
record.MojangSignature = event.MojangSignature
|
||||||
|
|
||||||
|
service.SkinsRepo.Save(record)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupClient(client *cony.Client) (<-chan error, <-chan error, <-chan amqp.Delivery ) {
|
||||||
|
exchange := cony.Exchange{
|
||||||
|
Name: exchangeName,
|
||||||
|
Kind: "topic",
|
||||||
|
Durable: true,
|
||||||
|
AutoDelete: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
queue := &cony.Queue{
|
||||||
|
Name: queueName,
|
||||||
|
Durable: true,
|
||||||
|
AutoDelete: false,
|
||||||
|
Exclusive: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
usernameEventBinding := cony.Binding{
|
||||||
|
Exchange: exchange,
|
||||||
|
Queue: queue,
|
||||||
|
Key: "accounts.username-changed",
|
||||||
|
}
|
||||||
|
|
||||||
|
skinEventBinding := cony.Binding{
|
||||||
|
Exchange: exchange,
|
||||||
|
Queue: queue,
|
||||||
|
Key: "accounts.skin-changed",
|
||||||
|
}
|
||||||
|
|
||||||
|
declarations := []cony.Declaration{
|
||||||
|
cony.DeclareExchange(exchange),
|
||||||
|
cony.DeclareQueue(queue),
|
||||||
|
cony.DeclareBinding(usernameEventBinding),
|
||||||
|
cony.DeclareBinding(skinEventBinding),
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Declare(declarations)
|
||||||
|
|
||||||
|
consumer := cony.NewConsumer(queue,
|
||||||
|
cony.Qos(10),
|
||||||
|
cony.AutoTag(),
|
||||||
|
)
|
||||||
|
client.Consume(consumer)
|
||||||
|
|
||||||
|
return client.Errors(), consumer.Errors(), consumer.Deliveries()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user