Implemented WebHooks delivery queue.

Completely removed usage of the RabbitMQ. Queue now based on Redis channels.
Worker process now extracted as separate docker container.
Base image upgraded to the 1.8.0 version (PHP 7.2.7 and pcntl extension).
This commit is contained in:
ErickSkrauch 2018-07-08 18:20:19 +03:00
parent 6751eb6591
commit c0aa78d156
55 changed files with 933 additions and 1684 deletions

View File

@ -29,13 +29,6 @@ REDIS_PORT=6379
REDIS_DATABASE=0 REDIS_DATABASE=0
REDIS_PASSWORD= REDIS_PASSWORD=
## Параметры подключения к rabbitmq
RABBITMQ_HOST=rabbitmq
RABBITMQ_PORT=5672
RABBITMQ_USER=ely-accounts-app
RABBITMQ_PASS=ely-accounts-app-password
RABBITMQ_VHOST=/ely.by
## Параметры Statsd ## Параметры Statsd
STATSD_HOST=statsd.ely.by STATSD_HOST=statsd.ely.by
STATSD_PORT=8125 STATSD_PORT=8125
@ -59,8 +52,3 @@ MYSQL_ROOT_PASSWORD=
MYSQL_DATABASE=ely_accounts MYSQL_DATABASE=ely_accounts
MYSQL_USER=ely_accounts_user MYSQL_USER=ely_accounts_user
MYSQL_PASSWORD=ely_accounts_password MYSQL_PASSWORD=ely_accounts_password
# RabbitMQ
RABBITMQ_DEFAULT_USER=ely-accounts-app
RABBITMQ_DEFAULT_PASS=ely-accounts-app-password
RABBITMQ_DEFAULT_VHOST=/ely.by

View File

@ -1,4 +1,4 @@
FROM registry.ely.by/elyby/accounts-php:1.7.0 FROM registry.ely.by/elyby/accounts-php:1.8.0
# bootstrap скрипт для проекта # bootstrap скрипт для проекта
COPY docker/php/bootstrap.sh /bootstrap.sh COPY docker/php/bootstrap.sh /bootstrap.sh

View File

@ -1,4 +1,4 @@
FROM registry.ely.by/elyby/accounts-php:1.7.0-dev FROM registry.ely.by/elyby/accounts-php:1.8.0-dev
# bootstrap скрипт для проекта # bootstrap скрипт для проекта
COPY docker/php/bootstrap.sh /bootstrap.sh COPY docker/php/bootstrap.sh /bootstrap.sh

View File

@ -1,9 +1,10 @@
<?php <?php
declare(strict_types=1);
namespace api\models\authentication; namespace api\models\authentication;
use api\aop\annotations\CollectModelMetrics; use api\aop\annotations\CollectModelMetrics;
use api\models\base\ApiForm; use api\models\base\ApiForm;
use api\modules\accounts\models\ChangeUsernameForm;
use api\validators\EmailActivationKeyValidator; use api\validators\EmailActivationKeyValidator;
use common\models\Account; use common\models\Account;
use common\models\EmailActivation; use common\models\EmailActivation;
@ -44,9 +45,6 @@ class ConfirmEmailForm extends ApiForm {
throw new ErrorException('Unable activate user account.'); throw new ErrorException('Unable activate user account.');
} }
$changeUsernameForm = new ChangeUsernameForm($account);
$changeUsernameForm->createEventTask($account->id, $account->username, null);
$transaction->commit(); $transaction->commit();
return Yii::$app->user->createJwtAuthenticationToken($account, true); return Yii::$app->user->createJwtAuthenticationToken($account, true);

View File

@ -1,13 +1,11 @@
<?php <?php
namespace api\modules\accounts\models; namespace api\modules\accounts\models;
use api\exceptions\ThisShouldNotHappenException;
use api\modules\internal\helpers\Error as E; use api\modules\internal\helpers\Error as E;
use common\helpers\Amqp;
use common\models\Account; use common\models\Account;
use common\models\amqp\AccountBanned; use common\tasks\ClearAccountSessions;
use PhpAmqpLib\Message\AMQPMessage;
use Yii; use Yii;
use yii\base\ErrorException;
class BanAccountForm extends AccountActionForm { class BanAccountForm extends AccountActionForm {
@ -38,7 +36,7 @@ class BanAccountForm extends AccountActionForm {
]; ];
} }
public function validateAccountActivity() { public function validateAccountActivity(): void {
if ($this->getAccount()->status === Account::STATUS_BANNED) { if ($this->getAccount()->status === Account::STATUS_BANNED) {
$this->addError('account', E::ACCOUNT_ALREADY_BANNED); $this->addError('account', E::ACCOUNT_ALREADY_BANNED);
} }
@ -54,27 +52,14 @@ class BanAccountForm extends AccountActionForm {
$account = $this->getAccount(); $account = $this->getAccount();
$account->status = Account::STATUS_BANNED; $account->status = Account::STATUS_BANNED;
if (!$account->save()) { if (!$account->save()) {
throw new ErrorException('Cannot ban account'); throw new ThisShouldNotHappenException('Cannot ban account');
} }
$this->createTask(); Yii::$app->queue->push(ClearAccountSessions::createFromAccount($account));
$transaction->commit(); $transaction->commit();
return true; return true;
} }
public function createTask(): void {
$model = new AccountBanned();
$model->accountId = $this->getAccount()->id;
$model->duration = $this->duration;
$model->message = $this->message;
$message = Amqp::getInstance()->prepareMessage($model, [
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
]);
Amqp::sendToEventsExchange('accounts.account-banned', $message);
}
} }

View File

@ -2,13 +2,10 @@
namespace api\modules\accounts\models; namespace api\modules\accounts\models;
use api\aop\annotations\CollectModelMetrics; use api\aop\annotations\CollectModelMetrics;
use api\exceptions\ThisShouldNotHappenException;
use api\validators\EmailActivationKeyValidator; use api\validators\EmailActivationKeyValidator;
use common\helpers\Amqp;
use common\models\amqp\EmailChanged;
use common\models\EmailActivation; use common\models\EmailActivation;
use PhpAmqpLib\Message\AMQPMessage;
use Yii; use Yii;
use yii\base\ErrorException;
class ChangeEmailForm extends AccountActionForm { class ChangeEmailForm extends AccountActionForm {
@ -35,30 +32,14 @@ class ChangeEmailForm extends AccountActionForm {
$activation->delete(); $activation->delete();
$account = $this->getAccount(); $account = $this->getAccount();
$oldEmail = $account->email;
$account->email = $activation->newEmail; $account->email = $activation->newEmail;
if (!$account->save()) { if (!$account->save()) {
throw new ErrorException('Cannot save new account email value'); throw new ThisShouldNotHappenException('Cannot save new account email value');
} }
$this->createTask($account->id, $account->email, $oldEmail);
$transaction->commit(); $transaction->commit();
return true; return true;
} }
public function createTask(int $accountId, string $newEmail, string $oldEmail): void {
$model = new EmailChanged();
$model->accountId = $accountId;
$model->oldEmail = $oldEmail;
$model->newEmail = $newEmail;
$message = Amqp::getInstance()->prepareMessage($model, [
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
]);
Amqp::sendToEventsExchange('accounts.email-changed', $message);
}
} }

View File

@ -4,13 +4,10 @@ namespace api\modules\accounts\models;
use api\aop\annotations\CollectModelMetrics; use api\aop\annotations\CollectModelMetrics;
use api\exceptions\ThisShouldNotHappenException; use api\exceptions\ThisShouldNotHappenException;
use api\validators\PasswordRequiredValidator; use api\validators\PasswordRequiredValidator;
use common\helpers\Amqp;
use common\models\amqp\UsernameChanged;
use common\models\UsernameHistory; use common\models\UsernameHistory;
use common\tasks\PullMojangUsername;
use common\validators\UsernameValidator; use common\validators\UsernameValidator;
use PhpAmqpLib\Message\AMQPMessage;
use Yii; use Yii;
use yii\base\ErrorException;
class ChangeUsernameForm extends AccountActionForm { class ChangeUsernameForm extends AccountActionForm {
@ -42,7 +39,6 @@ class ChangeUsernameForm extends AccountActionForm {
$transaction = Yii::$app->db->beginTransaction(); $transaction = Yii::$app->db->beginTransaction();
$oldNickname = $account->username;
$account->username = $this->username; $account->username = $this->username;
if (!$account->save()) { if (!$account->save()) {
throw new ThisShouldNotHappenException('Cannot save account model with new username'); throw new ThisShouldNotHappenException('Cannot save account model with new username');
@ -52,36 +48,14 @@ class ChangeUsernameForm extends AccountActionForm {
$usernamesHistory->account_id = $account->id; $usernamesHistory->account_id = $account->id;
$usernamesHistory->username = $account->username; $usernamesHistory->username = $account->username;
if (!$usernamesHistory->save()) { if (!$usernamesHistory->save()) {
throw new ErrorException('Cannot save username history record'); throw new ThisShouldNotHappenException('Cannot save username history record');
} }
$this->createEventTask($account->id, $account->username, $oldNickname); Yii::$app->queue->push(PullMojangUsername::createFromAccount($account));
$transaction->commit(); $transaction->commit();
return true; return true;
} }
/**
* TODO: вынести это в отдельную сущность, т.к. эта команда используется внутри формы регистрации
*
* @param integer $accountId
* @param string $newNickname
* @param string $oldNickname
*
* @throws \PhpAmqpLib\Exception\AMQPExceptionInterface|\yii\base\Exception
*/
public function createEventTask($accountId, $newNickname, $oldNickname): void {
$model = new UsernameChanged();
$model->accountId = $accountId;
$model->oldUsername = $oldNickname;
$model->newUsername = $newNickname;
$message = Amqp::getInstance()->prepareMessage($model, [
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
]);
Amqp::sendToEventsExchange('accounts.username-changed', $message);
}
} }

View File

@ -1,13 +1,10 @@
<?php <?php
namespace api\modules\accounts\models; namespace api\modules\accounts\models;
use api\exceptions\ThisShouldNotHappenException;
use api\modules\internal\helpers\Error as E; use api\modules\internal\helpers\Error as E;
use common\helpers\Amqp;
use common\models\Account; use common\models\Account;
use common\models\amqp\AccountPardoned;
use PhpAmqpLib\Message\AMQPMessage;
use Yii; use Yii;
use yii\base\ErrorException;
class PardonAccountForm extends AccountActionForm { class PardonAccountForm extends AccountActionForm {
@ -33,25 +30,12 @@ class PardonAccountForm extends AccountActionForm {
$account = $this->getAccount(); $account = $this->getAccount();
$account->status = Account::STATUS_ACTIVE; $account->status = Account::STATUS_ACTIVE;
if (!$account->save()) { if (!$account->save()) {
throw new ErrorException('Cannot pardon account'); throw new ThisShouldNotHappenException('Cannot pardon account');
} }
$this->createTask();
$transaction->commit(); $transaction->commit();
return true; return true;
} }
public function createTask(): void {
$model = new AccountPardoned();
$model->accountId = $this->getAccount()->id;
$message = Amqp::getInstance()->prepareMessage($model, [
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
]);
Amqp::sendToEventsExchange('accounts.account-pardoned', $message);
}
} }

View File

@ -56,10 +56,9 @@ class RateLimiter extends \yii\filters\RateLimiter {
$ip = $request->getUserIP(); $ip = $request->getUserIP();
$key = $this->buildKey($ip); $key = $this->buildKey($ip);
$redis = $this->getRedis(); $countRequests = (int)Yii::$app->redis->incr($key);
$countRequests = (int)$redis->incr($key);
if ($countRequests === 1) { if ($countRequests === 1) {
$redis->executeCommand('EXPIRE', [$key, $this->limitTime]); Yii::$app->redis->expire($key, $this->limitTime);
} }
if ($countRequests > $this->limit) { if ($countRequests > $this->limit) {
@ -67,13 +66,6 @@ class RateLimiter extends \yii\filters\RateLimiter {
} }
} }
/**
* @return \common\components\Redis\Connection
*/
public function getRedis() {
return Yii::$app->redis;
}
/** /**
* @param Request $request * @param Request $request
* @return OauthClient|null * @return OauthClient|null

View File

@ -19,7 +19,7 @@ class SessionModel {
public static function find(string $username, string $serverId): ?self { public static function find(string $username, string $serverId): ?self {
$key = static::buildKey($username, $serverId); $key = static::buildKey($username, $serverId);
$result = Yii::$app->redis->executeCommand('GET', [$key]); $result = Yii::$app->redis->get($key);
if (!$result) { if (!$result) {
return null; return null;
} }
@ -36,11 +36,11 @@ class SessionModel {
'serverId' => $this->serverId, 'serverId' => $this->serverId,
]); ]);
return Yii::$app->redis->executeCommand('SETEX', [$key, self::KEY_TIME, $data]); return Yii::$app->redis->setex($key, self::KEY_TIME, $data);
} }
public function delete() { public function delete() {
return Yii::$app->redis->executeCommand('DEL', [static::buildKey($this->username, $this->serverId)]); return Yii::$app->redis->del(static::buildKey($this->username, $this->serverId));
} }
public function getAccount(): ?Account { public function getAccount(): ?Account {

View File

@ -18,8 +18,7 @@ class Yii extends \yii\BaseYii {
* *
* @property \yii\db\Connection $unbufferedDb * @property \yii\db\Connection $unbufferedDb
* @property \yii\swiftmailer\Mailer $mailer * @property \yii\swiftmailer\Mailer $mailer
* @property \common\components\Redis\Connection $redis * @property \yii\redis\Connection $redis
* @property \common\components\RabbitMQ\Component $amqp
* @property \GuzzleHttp\Client $guzzle * @property \GuzzleHttp\Client $guzzle
* @property \common\components\EmailRenderer $emailRenderer * @property \common\components\EmailRenderer $emailRenderer
* @property \mito\sentry\Component $sentry * @property \mito\sentry\Component $sentry

View File

@ -1,177 +0,0 @@
<?php
namespace common\components\RabbitMQ;
use PhpAmqpLib\Channel\AMQPChannel;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
use yii\base\Exception;
use yii\helpers\Json;
/**
* Не гибкий компонент для работы с RabbitMQ, заточенный под нужны текущего проекта
*
* Компонент основан на расширении Alexey Kuznetsov <mirakuru@webtoucher.ru>
*
* @property AMQPStreamConnection $connection AMQP connection.
* @property AMQPChannel $channel AMQP channel.
*/
class Component extends \yii\base\Component {
public const TYPE_TOPIC = 'topic';
public const TYPE_DIRECT = 'direct';
public const TYPE_HEADERS = 'headers';
public const TYPE_FANOUT = 'fanout';
/**
* @var string
*/
public $host = '127.0.0.1';
/**
* @var integer
*/
public $port = 5672;
/**
* @var string
*/
public $user;
/**
* @var string
*/
public $password;
/**
* @var string
*/
public $vhost = '/';
/**
* @var AMQPStreamConnection
*/
protected $amqpConnection;
/**
* @var AMQPChannel[]
*/
protected $channels = [];
/**
* @inheritdoc
*/
public function init() {
parent::init();
if (empty($this->user)) {
throw new Exception("Parameter 'user' was not set for AMQP connection.");
}
}
/**
* @return AMQPStreamConnection
*/
public function getConnection() {
if (!$this->amqpConnection) {
$this->amqpConnection = new AMQPStreamConnection(
$this->host,
$this->port,
$this->user,
$this->password,
$this->vhost
);
}
return $this->amqpConnection;
}
/**
* @param string $channel_id
* @return AMQPChannel
*/
public function getChannel($channel_id = null) {
$index = $channel_id ?: 'default';
if (!array_key_exists($index, $this->channels)) {
$this->channels[$index] = $this->getConnection()->channel($channel_id);
}
return $this->channels[$index];
}
// TODO: метод sendToQueue
/**
* Sends message to the exchange.
*
* @param string $exchangeName
* @param string $routingKey
* @param string|array $message
* @param array $exchangeArgs
* @param array $publishArgs
*/
public function sendToExchange($exchangeName, $routingKey, $message, $exchangeArgs = [], $publishArgs = []) {
$message = $this->prepareMessage($message);
$channel = $this->getChannel();
$channel->exchange_declare(...$this->prepareExchangeArgs($exchangeName, $exchangeArgs));
$channel->basic_publish(...$this->preparePublishArgs($message, $exchangeName, $routingKey, $publishArgs));
}
/**
* Returns prepaired AMQP message.
*
* @param string|array|object $message
* @param array $properties
* @return AMQPMessage
* @throws Exception If message is empty.
*/
public function prepareMessage($message, $properties = null) {
if ($message instanceof AMQPMessage) {
return $message;
}
if (empty($message)) {
throw new Exception('AMQP message can not be empty');
}
if (is_array($message) || is_object($message)) {
$message = Json::encode($message);
}
return new AMQPMessage($message, $properties);
}
/**
* Объединяет переданный набор аргументов с поведением по умолчанию
*
* @param string $exchangeName
* @param array $args
* @return array
*/
protected function prepareExchangeArgs($exchangeName, array $args) {
return array_replace([
$exchangeName,
self::TYPE_FANOUT,
false,
false,
false,
], $args);
}
/**
* Объединяет переданный набор аргументов с поведением по умолчанию
*
* @param AMQPMessage $message
* @param string $exchangeName
* @param string $routeKey
* @param array $args
*
* @return array
*/
protected function preparePublishArgs($message, $exchangeName, $routeKey, array $args) {
return array_replace([
$message,
$exchangeName,
$routeKey,
], $args);
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace common\components\RabbitMQ;
use Yii;
class Helper {
/**
* @return Component $amqp
*/
public static function getInstance() {
return Yii::$app->amqp;
}
public static function sendToExchange($exchange, $routingKey, $message, $exchangeArgs = []) {
static::getInstance()->sendToExchange($exchange, $routingKey, $message, $exchangeArgs);
}
public static function sendToEventsExchange($routingKey, $message) {
static::sendToExchange('events', $routingKey, $message, [
1 => Component::TYPE_TOPIC, // type -> topic
3 => true, // durable -> true
]);
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace common\components\Redis;
use yii\di\Instance;
class Cache extends \yii\redis\Cache {
public function init() {
\yii\caching\Cache::init();
$this->redis = Instance::ensure($this->redis, ConnectionInterface::class);
}
}

View File

@ -1,415 +0,0 @@
<?php
namespace common\components\Redis;
use Predis\Client;
use Predis\ClientInterface;
use yii\base\Component;
/**
* Interface defining a client able to execute commands against Redis.
*
* All the commands exposed by the client generally have the same signature as
* described by the Redis documentation, but some of them offer an additional
* and more friendly interface to ease programming which is described in the
* following list of methods:
*
* @method int del(array $keys)
* @method string dump($key)
* @method int exists($key)
* @method int expire($key, $seconds)
* @method int expireat($key, $timestamp)
* @method array keys($pattern)
* @method int move($key, $db)
* @method mixed object($subcommand, $key)
* @method int persist($key)
* @method int pexpire($key, $milliseconds)
* @method int pexpireat($key, $timestamp)
* @method int pttl($key)
* @method string randomkey()
* @method mixed rename($key, $target)
* @method int renamenx($key, $target)
* @method array scan($cursor, array $options = null)
* @method array sort($key, array $options = null)
* @method int ttl($key)
* @method mixed type($key)
* @method int append($key, $value)
* @method int bitcount($key, $start = null, $end = null)
* @method int bitop($operation, $destkey, $key)
* @method int decr($key)
* @method int decrby($key, $decrement)
* @method string get($key)
* @method int getbit($key, $offset)
* @method string getrange($key, $start, $end)
* @method string getset($key, $value)
* @method int incr($key)
* @method int incrby($key, $increment)
* @method string incrbyfloat($key, $increment)
* @method array mget(array $keys)
* @method mixed mset(array $dictionary)
* @method int msetnx(array $dictionary)
* @method mixed psetex($key, $milliseconds, $value)
* @method mixed set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
* @method int setbit($key, $offset, $value)
* @method int setex($key, $seconds, $value)
* @method int setnx($key, $value)
* @method int setrange($key, $offset, $value)
* @method int strlen($key)
* @method int hdel($key, array $fields)
* @method int hexists($key, $field)
* @method string hget($key, $field)
* @method array hgetall($key)
* @method int hincrby($key, $field, $increment)
* @method string hincrbyfloat($key, $field, $increment)
* @method array hkeys($key)
* @method int hlen($key)
* @method array hmget($key, array $fields)
* @method mixed hmset($key, array $dictionary)
* @method array hscan($key, $cursor, array $options = null)
* @method int hset($key, $field, $value)
* @method int hsetnx($key, $field, $value)
* @method array hvals($key)
* @method array blpop(array $keys, $timeout)
* @method array brpop(array $keys, $timeout)
* @method array brpoplpush($source, $destination, $timeout)
* @method string lindex($key, $index)
* @method int linsert($key, $whence, $pivot, $value)
* @method int llen($key)
* @method string lpop($key)
* @method int lpush($key, array $values)
* @method int lpushx($key, $value)
* @method array lrange($key, $start, $stop)
* @method int lrem($key, $count, $value)
* @method mixed lset($key, $index, $value)
* @method mixed ltrim($key, $start, $stop)
* @method string rpop($key)
* @method string rpoplpush($source, $destination)
* @method int rpush($key, array $values)
* @method int rpushx($key, $value)
* @method int sadd($key, array $members)
* @method int scard($key)
* @method array sdiff(array $keys)
* @method int sdiffstore($destination, array $keys)
* @method array sinter(array $keys)
* @method int sinterstore($destination, array $keys)
* @method int sismember($key, $member)
* @method array smembers($key)
* @method int smove($source, $destination, $member)
* @method string spop($key)
* @method string srandmember($key, $count = null)
* @method int srem($key, $member)
* @method array sscan($key, $cursor, array $options = null)
* @method array sunion(array $keys)
* @method int sunionstore($destination, array $keys)
* @method int zadd($key, array $membersAndScoresDictionary)
* @method int zcard($key)
* @method string zcount($key, $min, $max)
* @method string zincrby($key, $increment, $member)
* @method int zinterstore($destination, array $keys, array $options = null)
* @method array zrange($key, $start, $stop, array $options = null)
* @method array zrangebyscore($key, $min, $max, array $options = null)
* @method int zrank($key, $member)
* @method int zrem($key, $member)
* @method int zremrangebyrank($key, $start, $stop)
* @method int zremrangebyscore($key, $min, $max)
* @method array zrevrange($key, $start, $stop, array $options = null)
* @method array zrevrangebyscore($key, $min, $max, array $options = null)
* @method int zrevrank($key, $member)
* @method int zunionstore($destination, array $keys, array $options = null)
* @method string zscore($key, $member)
* @method array zscan($key, $cursor, array $options = null)
* @method array zrangebylex($key, $start, $stop, array $options = null)
* @method int zremrangebylex($key, $min, $max)
* @method int zlexcount($key, $min, $max)
* @method int pfadd($key, array $elements)
* @method mixed pfmerge($destinationKey, array $sourceKeys)
* @method int pfcount(array $keys)
* @method mixed pubsub($subcommand, $argument)
* @method int publish($channel, $message)
* @method mixed discard()
* @method array exec()
* @method mixed multi()
* @method mixed unwatch()
* @method mixed watch($key)
* @method mixed eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
* @method mixed evalsha($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
* @method mixed script($subcommand, $argument = null)
* @method mixed auth($password)
* @method string echo($message)
* @method mixed ping($message = null)
* @method mixed select($database)
* @method mixed bgrewriteaof()
* @method mixed bgsave()
* @method mixed client($subcommand, $argument = null)
* @method mixed config($subcommand, $argument = null)
* @method int dbsize()
* @method mixed flushall()
* @method mixed flushdb()
* @method array info($section = null)
* @method int lastsave()
* @method mixed save()
* @method mixed slaveof($host, $port)
* @method mixed slowlog($subcommand, $argument = null)
* @method array time()
* @method array command()
*/
class Connection extends Component implements ConnectionInterface {
/**
* @var array List of available redis commands http://redis.io/commands
*/
public const REDIS_COMMANDS = [
'BLPOP', // key [key ...] timeout Remove and get the first element in a list, or block until one is available
'BRPOP', // key [key ...] timeout Remove and get the last element in a list, or block until one is available
'BRPOPLPUSH', // source destination timeout Pop a value from a list, push it to another list and return it; or block until one is available
'CLIENT KILL', // ip:port Kill the connection of a client
'CLIENT LIST', // Get the list of client connections
'CLIENT GETNAME', // Get the current connection name
'CLIENT SETNAME', // connection-name Set the current connection name
'CONFIG GET', // parameter Get the value of a configuration parameter
'CONFIG SET', // parameter value Set a configuration parameter to the given value
'CONFIG RESETSTAT', // Reset the stats returned by INFO
'DBSIZE', // Return the number of keys in the selected database
'DEBUG OBJECT', // key Get debugging information about a key
'DEBUG SEGFAULT', // Make the server crash
'DECR', // key Decrement the integer value of a key by one
'DECRBY', // key decrement Decrement the integer value of a key by the given number
'DEL', // key [key ...] Delete a key
'DISCARD', // Discard all commands issued after MULTI
'DUMP', // key Return a serialized version of the value stored at the specified key.
'ECHO', // message Echo the given string
'EVAL', // script numkeys key [key ...] arg [arg ...] Execute a Lua script server side
'EVALSHA', // sha1 numkeys key [key ...] arg [arg ...] Execute a Lua script server side
'EXEC', // Execute all commands issued after MULTI
'EXISTS', // key Determine if a key exists
'EXPIRE', // key seconds Set a key's time to live in seconds
'EXPIREAT', // key timestamp Set the expiration for a key as a UNIX timestamp
'FLUSHALL', // Remove all keys from all databases
'FLUSHDB', // Remove all keys from the current database
'GET', // key Get the value of a key
'GETBIT', // key offset Returns the bit value at offset in the string value stored at key
'GETRANGE', // key start end Get a substring of the string stored at a key
'GETSET', // key value Set the string value of a key and return its old value
'HDEL', // key field [field ...] Delete one or more hash fields
'HEXISTS', // key field Determine if a hash field exists
'HGET', // key field Get the value of a hash field
'HGETALL', // key Get all the fields and values in a hash
'HINCRBY', // key field increment Increment the integer value of a hash field by the given number
'HINCRBYFLOAT', // key field increment Increment the float value of a hash field by the given amount
'HKEYS', // key Get all the fields in a hash
'HLEN', // key Get the number of fields in a hash
'HMGET', // key field [field ...] Get the values of all the given hash fields
'HMSET', // key field value [field value ...] Set multiple hash fields to multiple values
'HSET', // key field value Set the string value of a hash field
'HSETNX', // key field value Set the value of a hash field, only if the field does not exist
'HVALS', // key Get all the values in a hash
'INCR', // key Increment the integer value of a key by one
'INCRBY', // key increment Increment the integer value of a key by the given amount
'INCRBYFLOAT', // key increment Increment the float value of a key by the given amount
'INFO', // [section] Get information and statistics about the server
'KEYS', // pattern Find all keys matching the given pattern
'LASTSAVE', // Get the UNIX time stamp of the last successful save to disk
'LINDEX', // key index Get an element from a list by its index
'LINSERT', // key BEFORE|AFTER pivot value Insert an element before or after another element in a list
'LLEN', // key Get the length of a list
'LPOP', // key Remove and get the first element in a list
'LPUSH', // key value [value ...] Prepend one or multiple values to a list
'LPUSHX', // key value Prepend a value to a list, only if the list exists
'LRANGE', // key start stop Get a range of elements from a list
'LREM', // key count value Remove elements from a list
'LSET', // key index value Set the value of an element in a list by its index
'LTRIM', // key start stop Trim a list to the specified range
'MGET', // key [key ...] Get the values of all the given keys
'MIGRATE', // host port key destination-db timeout Atomically transfer a key from a Redis instance to another one.
'MONITOR', // Listen for all requests received by the server in real time
'MOVE', // key db Move a key to another database
'MSET', // key value [key value ...] Set multiple keys to multiple values
'MSETNX', // key value [key value ...] Set multiple keys to multiple values, only if none of the keys exist
'MULTI', // Mark the start of a transaction block
'OBJECT', // subcommand [arguments [arguments ...]] Inspect the internals of Redis objects
'PERSIST', // key Remove the expiration from a key
'PEXPIRE', // key milliseconds Set a key's time to live in milliseconds
'PEXPIREAT', // key milliseconds-timestamp Set the expiration for a key as a UNIX timestamp specified in milliseconds
'PING', // Ping the server
'PSETEX', // key milliseconds value Set the value and expiration in milliseconds of a key
'PSUBSCRIBE', // pattern [pattern ...] Listen for messages published to channels matching the given patterns
'PTTL', // key Get the time to live for a key in milliseconds
'PUBLISH', // channel message Post a message to a channel
'PUNSUBSCRIBE', // [pattern [pattern ...]] Stop listening for messages posted to channels matching the given patterns
'QUIT', // Close the connection
'RANDOMKEY', // Return a random key from the keyspace
'RENAME', // key newkey Rename a key
'RENAMENX', // key newkey Rename a key, only if the new key does not exist
'RESTORE', // key ttl serialized-value Create a key using the provided serialized value, previously obtained using DUMP.
'RPOP', // key Remove and get the last element in a list
'RPOPLPUSH', // source destination Remove the last element in a list, append it to another list and return it
'RPUSH', // key value [value ...] Append one or multiple values to a list
'RPUSHX', // key value Append a value to a list, only if the list exists
'SADD', // key member [member ...] Add one or more members to a set
'SAVE', // Synchronously save the dataset to disk
'SCARD', // key Get the number of members in a set
'SCRIPT EXISTS', // script [script ...] Check existence of scripts in the script cache.
'SCRIPT FLUSH', // Remove all the scripts from the script cache.
'SCRIPT KILL', // Kill the script currently in execution.
'SCRIPT LOAD', // script Load the specified Lua script into the script cache.
'SDIFF', // key [key ...] Subtract multiple sets
'SDIFFSTORE', // destination key [key ...] Subtract multiple sets and store the resulting set in a key
'SELECT', // index Change the selected database for the current connection
'SET', // key value Set the string value of a key
'SETBIT', // key offset value Sets or clears the bit at offset in the string value stored at key
'SETEX', // key seconds value Set the value and expiration of a key
'SETNX', // key value Set the value of a key, only if the key does not exist
'SETRANGE', // key offset value Overwrite part of a string at key starting at the specified offset
'SHUTDOWN', // [NOSAVE] [SAVE] Synchronously save the dataset to disk and then shut down the server
'SINTER', // key [key ...] Intersect multiple sets
'SINTERSTORE', // destination key [key ...] Intersect multiple sets and store the resulting set in a key
'SISMEMBER', // key member Determine if a given value is a member of a set
'SLAVEOF', // host port Make the server a slave of another instance, or promote it as master
'SLOWLOG', // subcommand [argument] Manages the Redis slow queries log
'SMEMBERS', // key Get all the members in a set
'SMOVE', // source destination member Move a member from one set to another
'SORT', // key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination] Sort the elements in a list, set or sorted set
'SPOP', // key Remove and return a random member from a set
'SRANDMEMBER', // key [count] Get one or multiple random members from a set
'SREM', // key member [member ...] Remove one or more members from a set
'STRLEN', // key Get the length of the value stored in a key
'SUBSCRIBE', // channel [channel ...] Listen for messages published to the given channels
'SUNION', // key [key ...] Add multiple sets
'SUNIONSTORE', // destination key [key ...] Add multiple sets and store the resulting set in a key
'SYNC', // Internal command used for replication
'TIME', // Return the current server time
'TTL', // key Get the time to live for a key
'TYPE', // key Determine the type stored at key
'UNSUBSCRIBE', // [channel [channel ...]] Stop listening for messages posted to the given channels
'UNWATCH', // Forget about all watched keys
'WATCH', // key [key ...] Watch the given keys to determine execution of the MULTI/EXEC block
'ZADD', // key score member [score member ...] Add one or more members to a sorted set, or update its score if it already exists
'ZCARD', // key Get the number of members in a sorted set
'ZCOUNT', // key min max Count the members in a sorted set with scores within the given values
'ZINCRBY', // key increment member Increment the score of a member in a sorted set
'ZINTERSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Intersect multiple sorted sets and store the resulting sorted set in a new key
'ZRANGE', // key start stop [WITHSCORES] Return a range of members in a sorted set, by index
'ZRANGEBYSCORE', // key min max [WITHSCORES] [LIMIT offset count] Return a range of members in a sorted set, by score
'ZRANK', // key member Determine the index of a member in a sorted set
'ZREM', // key member [member ...] Remove one or more members from a sorted set
'ZREMRANGEBYRANK', // key start stop Remove all members in a sorted set within the given indexes
'ZREMRANGEBYSCORE', // key min max Remove all members in a sorted set within the given scores
'ZREVRANGE', // key start stop [WITHSCORES] Return a range of members in a sorted set, by index, with scores ordered from high to low
'ZREVRANGEBYSCORE', // key max min [WITHSCORES] [LIMIT offset count] Return a range of members in a sorted set, by score, with scores ordered from high to low
'ZREVRANK', // key member Determine the index of a member in a sorted set, with scores ordered from high to low
'ZSCORE', // key member Get the score associated with the given member in a sorted set
'ZUNIONSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Add multiple sorted sets and store the resulting sorted set in a new key
'GEOADD', // key longitude latitude member [longitude latitude member ...] Add point
'GEODIST', // key member1 member2 [unit] Return the distance between two members
'GEOHASH', // key member [member ...] Return valid Geohash strings
'GEOPOS', // key member [member ...] Return the positions (longitude,latitude)
'GEORADIUS', // key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] Return the members
'GEORADIUSBYMEMBER', // key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
];
/**
* @var string the hostname or ip address to use for connecting to the redis server. Defaults to 'localhost'.
* If [[unixSocket]] is specified, hostname and port will be ignored.
*/
public $hostname = 'localhost';
/**
* @var integer the port to use for connecting to the redis server. Default port is 6379.
* If [[unixSocket]] is specified, hostname and port will be ignored.
*/
public $port = 6379;
/**
* @var string the unix socket path (e.g. `/var/run/redis/redis.sock`) to use for connecting to the redis server.
* This can be used instead of [[hostname]] and [[port]] to connect to the server using a unix socket.
* If a unix socket path is specified, [[hostname]] and [[port]] will be ignored.
*/
public $unixSocket;
/**
* @var string the password for establishing DB connection. Defaults to null meaning no AUTH command is send.
* See http://redis.io/commands/auth
*/
public $password;
/**
* @var integer the redis database to use. This is an integer value starting from 0. Defaults to 0.
*/
public $database = 0;
/**
* @var float timeout to use for connection to redis. If not set the timeout set in php.ini will be used: ini_get("default_socket_timeout")
*/
public $connectionTimeout;
/**
* @var float timeout to use for redis socket when reading and writing data. If not set the php default value will be used.
*/
public $dataTimeout;
/**
* @var integer Bitmask field which may be set to any combination of connection flags passed to [stream_socket_client()](http://php.net/manual/en/function.stream-socket-client.php).
* Currently the select of connection flags is limited to `STREAM_CLIENT_CONNECT` (default), `STREAM_CLIENT_ASYNC_CONNECT` and `STREAM_CLIENT_PERSISTENT`.
* @see http://php.net/manual/en/function.stream-socket-client.php
*/
public $socketClientFlags = STREAM_CLIENT_CONNECT;
/**
* @var array|null
*/
public $parameters;
/**
* @var array
*/
public $options = [];
/**
* @var ClientInterface
*/
private $_client;
public function __call($name, $params) {
$redisCommand = mb_strtoupper($name);
if (in_array($redisCommand, self::REDIS_COMMANDS)) {
return $this->executeCommand($name, $params);
}
return parent::__call($name, $params);
}
public function getConnection(): ClientInterface {
if ($this->_client === null) {
$this->_client = new Client($this->prepareParams(), $this->options);
}
return $this->_client;
}
public function executeCommand(string $name, array $params = []) {
return $this->getConnection()->$name(...$params);
}
private function prepareParams() {
if ($this->parameters !== null) {
return $this->parameters;
}
if ($this->unixSocket) {
$parameters = [
'scheme' => 'unix',
'path' => $this->unixSocket,
];
} else {
$parameters = [
'scheme' => 'tcp',
'host' => $this->hostname,
'port' => $this->port,
];
}
return array_merge($parameters, [
'database' => $this->database,
]);
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace common\components\Redis;
interface ConnectionInterface {
/**
* @return ConnectionInterface
*/
public function getConnection();
/**
* @param string $name Command, that should be executed
* @param array $params Arguments for this command
*
* @return mixed
*/
public function executeCommand(string $name, array $params = []);
}

View File

@ -16,43 +16,35 @@ class Key {
$this->key = $this->buildKey($key); $this->key = $this->buildKey($key);
} }
public function getRedis(): Connection {
return Yii::$app->redis;
}
public function getKey(): string { public function getKey(): string {
return $this->key; return $this->key;
} }
public function getValue() { public function getValue() {
return $this->getRedis()->get($this->key); return Yii::$app->redis->get($this->key);
} }
public function setValue($value): self { public function setValue($value): self {
$this->getRedis()->set($this->key, $value); Yii::$app->redis->set($this->key, $value);
return $this; return $this;
} }
public function delete(): self { public function delete(): self {
$this->getRedis()->del([$this->getKey()]); Yii::$app->redis->del($this->getKey());
return $this; return $this;
} }
public function exists(): bool { public function exists(): bool {
return (bool)$this->getRedis()->exists($this->key); return (bool)Yii::$app->redis->exists($this->key);
} }
public function expire(int $ttl): self { public function expire(int $ttl): self {
$this->getRedis()->expire($this->key, $ttl); Yii::$app->redis->expire($this->key, $ttl);
return $this; return $this;
} }
public function expireAt(int $unixTimestamp): self { public function expireAt(int $unixTimestamp): self {
$this->getRedis()->expireat($this->key, $unixTimestamp); Yii::$app->redis->expireat($this->key, $unixTimestamp);
return $this; return $this;
} }

View File

@ -3,23 +3,22 @@ namespace common\components\Redis;
use ArrayIterator; use ArrayIterator;
use IteratorAggregate; use IteratorAggregate;
use Yii;
class Set extends Key implements IteratorAggregate { class Set extends Key implements IteratorAggregate {
public function add($value): self { public function add($value): self {
$this->getRedis()->sadd($this->getKey(), $value); Yii::$app->redis->sadd($this->getKey(), $value);
return $this; return $this;
} }
public function remove($value): self { public function remove($value): self {
$this->getRedis()->srem($this->getKey(), $value); Yii::$app->redis->srem($this->getKey(), $value);
return $this; return $this;
} }
public function members(): array { public function members(): array {
return $this->getRedis()->smembers($this->getKey()); return Yii::$app->redis->smembers($this->getKey());
} }
public function getValue(): array { public function getValue(): array {
@ -31,11 +30,11 @@ class Set extends Key implements IteratorAggregate {
return parent::exists(); return parent::exists();
} }
return (bool)$this->getRedis()->sismember($this->getKey(), $value); return (bool)Yii::$app->redis->sismember($this->getKey(), $value);
} }
public function diff(array $sets): array { public function diff(array $sets): array {
return $this->getRedis()->sdiff([$this->getKey(), implode(' ', $sets)]); return Yii::$app->redis->sdiff([$this->getKey(), implode(' ', $sets)]);
} }
/** /**

View File

@ -4,8 +4,7 @@ return [
'vendorPath' => dirname(__DIR__, 2) . '/vendor', 'vendorPath' => dirname(__DIR__, 2) . '/vendor',
'components' => [ 'components' => [
'cache' => [ 'cache' => [
'class' => common\components\Redis\Cache::class, 'class' => yii\redis\Cache::class,
'redis' => 'redis',
], ],
'db' => [ 'db' => [
'class' => yii\db\Connection::class, 'class' => yii\db\Connection::class,
@ -61,20 +60,12 @@ return [
'passwordHashStrategy' => 'password_hash', 'passwordHashStrategy' => 'password_hash',
], ],
'redis' => [ 'redis' => [
'class' => common\components\Redis\Connection::class, 'class' => yii\redis\Connection::class,
'hostname' => getenv('REDIS_HOST') ?: 'redis', 'hostname' => getenv('REDIS_HOST') ?: 'redis',
'password' => getenv('REDIS_PASS') ?: null, 'password' => getenv('REDIS_PASS') ?: null,
'port' => getenv('REDIS_PORT') ?: 6379, 'port' => getenv('REDIS_PORT') ?: 6379,
'database' => getenv('REDIS_DATABASE') ?: 0, 'database' => getenv('REDIS_DATABASE') ?: 0,
], ],
'amqp' => [
'class' => common\components\RabbitMQ\Component::class,
'host' => getenv('RABBITMQ_HOST') ?: 'rabbitmq',
'port' => getenv('RABBITMQ_PORT') ?: 5672,
'user' => getenv('RABBITMQ_USER'),
'password' => getenv('RABBITMQ_PASS'),
'vhost' => getenv('RABBITMQ_VHOST'),
],
'guzzle' => [ 'guzzle' => [
'class' => GuzzleHttp\Client::class, 'class' => GuzzleHttp\Client::class,
], ],
@ -97,15 +88,7 @@ return [
'namespace' => getenv('STATSD_NAMESPACE') ?: 'ely.accounts.' . gethostname() . '.app', 'namespace' => getenv('STATSD_NAMESPACE') ?: 'ely.accounts.' . gethostname() . '.app',
], ],
'queue' => [ 'queue' => [
'class' => yii\queue\amqp_interop\Queue::class, 'class' => yii\queue\redis\Queue::class,
'driver' => yii\queue\amqp_interop\Queue::ENQUEUE_AMQP_LIB,
'host' => getenv('RABBITMQ_HOST') ?: 'rabbitmq',
'port' => getenv('RABBITMQ_PORT') ?: 5672,
'user' => getenv('RABBITMQ_USER'),
'password' => getenv('RABBITMQ_PASS'),
'vhost' => getenv('RABBITMQ_VHOST'),
'queueName' => 'worker',
'exchangeName' => 'tasks',
], ],
], ],
'container' => [ 'container' => [

View File

@ -1,7 +1,10 @@
<?php <?php
declare(strict_types=1);
namespace common\models; namespace common\models;
use common\components\UserPass; use common\components\UserPass;
use common\tasks\CreateWebHooksDeliveries;
use Yii; use Yii;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\behaviors\TimestampBehavior; use yii\behaviors\TimestampBehavior;
@ -152,4 +155,22 @@ class Account extends ActiveRecord {
return $this->registration_ip === null ? null : inet_ntop($this->registration_ip); return $this->registration_ip === null ? null : inet_ntop($this->registration_ip);
} }
public function afterSave($insert, $changedAttributes) {
parent::afterSave($insert, $changedAttributes);
if ($insert) {
return;
}
$meaningfulFields = ['username', 'email', 'uuid', 'status', 'lang'];
$meaningfulChangedAttributes = array_filter($changedAttributes, function(string $key) use ($meaningfulFields) {
return in_array($key, $meaningfulFields, true);
}, ARRAY_FILTER_USE_KEY);
if (empty($meaningfulChangedAttributes)) {
return;
}
Yii::$app->queue->push(CreateWebHooksDeliveries::createAccountEdit($this, $meaningfulChangedAttributes));
}
} }

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace common\tasks;
use common\models\Account;
use Yii;
use yii\queue\RetryableJobInterface;
class ClearAccountSessions implements RetryableJobInterface {
public $accountId;
public static function createFromAccount(Account $account): self {
$result = new static();
$result->accountId = $account->id;
return $result;
}
/**
* @return int time to reserve in seconds
*/
public function getTtr(): int {
return 5 * 60;
}
/**
* @param int $attempt number
* @param \Exception|\Throwable $error from last execute of the job
*
* @return bool
*/
public function canRetry($attempt, $error): bool {
return true;
}
/**
* @param \yii\queue\Queue $queue which pushed and is handling the job
* @throws \Exception
*/
public function execute($queue): void {
$account = Account::findOne($this->accountId);
if ($account === null) {
return;
}
foreach ($account->getSessions()->each(100, Yii::$app->unbufferedDb) as $authSession) {
/** @var \common\models\AccountSession $authSession */
$authSession->delete();
}
foreach ($account->getMinecraftAccessKeys()->each(100, Yii::$app->unbufferedDb) as $key) {
/** @var \common\models\MinecraftAccessKey $key */
$key->delete();
}
foreach ($account->getOauthSessions()->each(100, Yii::$app->unbufferedDb) as $oauthSession) {
/** @var \common\models\OauthSession $oauthSession */
$oauthSession->delete();
}
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace common\tasks;
use common\models\Account;
use common\models\WebHook;
use Yii;
use yii\queue\RetryableJobInterface;
class CreateWebHooksDeliveries implements RetryableJobInterface {
/**
* @var string
*/
public $type;
/**
* @var array
*/
public $payloads;
public static function createAccountEdit(Account $account, array $changedAttributes): self {
$result = new static();
$result->type = 'account.edit';
$result->payloads = [
'id' => $account->id,
'uuid' => $account->uuid,
'username' => $account->username,
'email' => $account->email,
'lang' => $account->lang,
'isActive' => $account->status === Account::STATUS_ACTIVE,
'registered' => date('c', (int)$account->created_at),
'changedAttributes' => $changedAttributes,
];
return $result;
}
/**
* @return int time to reserve in seconds
*/
public function getTtr() {
return 10;
}
/**
* @param int $attempt number
* @param \Exception|\Throwable $error from last execute of the job
*
* @return bool
*/
public function canRetry($attempt, $error) {
return true;
}
/**
* @param \yii\queue\Queue $queue which pushed and is handling the job
*/
public function execute($queue) {
/** @var WebHook[] $targets */
$targets = WebHook::find()
->joinWith('events e', false)
->andWhere(['e.event_type' => $this->type])
->all();
foreach ($targets as $target) {
$job = new DeliveryWebHook();
$job->type = $this->type;
$job->url = $target->url;
$job->secret = $target->secret;
$job->payloads = $this->payloads;
Yii::$app->queue->push($job);
}
}
}

View File

@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace common\tasks;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
use Yii;
use yii\queue\RetryableJobInterface;
class DeliveryWebHook implements RetryableJobInterface {
/**
* @var string
*/
public $type;
/**
* @var string
*/
public $url;
/**
* @var string|null
*/
public $secret;
/**
* @var array
*/
public $payloads;
/**
* @return int time to reserve in seconds
*/
public function getTtr(): int {
return 65;
}
/**
* @param int $attempt number
* @param \Exception|\Throwable $error from last execute of the job
*
* @return bool
*/
public function canRetry($attempt, $error): bool {
if ($attempt >= 5) {
return false;
}
if ($error instanceof ServerException || $error instanceof ConnectException) {
return true;
}
return false;
}
/**
* @param \yii\queue\Queue $queue which pushed and is handling the job
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function execute($queue): void {
$client = $this->createClient();
try {
$client->request('POST', $this->url, [
'headers' => [
'User-Agent' => 'Account-Ely-Hookshot/' . Yii::$app->version,
'X-Ely-Accounts-Event' => $this->type,
],
'form_params' => $this->payloads,
]);
} catch (ClientException $e) {
Yii::info("Delivery for {$this->url} has failed with {$e->getResponse()->getStatusCode()} status.");
return;
}
}
protected function createClient(): ClientInterface {
return new GuzzleClient([
'handler' => $this->createStack(),
'timeout' => 60,
'connect_timeout' => 10,
]);
}
protected function createStack(): HandlerStack {
$stack = HandlerStack::create();
$stack->push(Middleware::mapRequest(function(RequestInterface $request): RequestInterface {
if (empty($this->secret)) {
return $request;
}
$payload = (string)$request->getBody();
$signature = hash_hmac('sha1', $payload, $this->secret);
/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
return $request->withHeader('X-Hub-Signature', 'sha1=' . $signature);
}));
return $stack;
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace common\tasks;
use api\exceptions\ThisShouldNotHappenException;
use common\components\Mojang\Api as MojangApi;
use common\components\Mojang\exceptions\MojangApiException;
use common\components\Mojang\exceptions\NoContentException;
use common\models\Account;
use common\models\MojangUsername;
use GuzzleHttp\Exception\RequestException;
use Yii;
use yii\queue\JobInterface;
class PullMojangUsername implements JobInterface {
public $username;
public static function createFromAccount(Account $account): self {
$result = new static();
$result->username = $account->username;
return $result;
}
/**
* @param \yii\queue\Queue $queue which pushed and is handling the job
*
* @throws \Exception
*/
public function execute($queue) {
Yii::$app->statsd->inc('queue.pullMojangUsername.attempt');
$mojangApi = $this->createMojangApi();
try {
$response = $mojangApi->usernameToUUID($this->username);
Yii::$app->statsd->inc('queue.pullMojangUsername.found');
} catch (NoContentException $e) {
$response = false;
Yii::$app->statsd->inc('queue.pullMojangUsername.not_found');
} catch (RequestException | MojangApiException $e) {
Yii::$app->statsd->inc('queue.pullMojangUsername.error');
return;
}
/** @var MojangUsername|null $mojangUsername */
$mojangUsername = MojangUsername::findOne($this->username);
if ($response === false) {
if ($mojangUsername !== null) {
$mojangUsername->delete();
}
} else {
if ($mojangUsername === null) {
$mojangUsername = new MojangUsername();
$mojangUsername->username = $response->name;
$mojangUsername->uuid = $response->id;
} else {
$mojangUsername->uuid = $response->id;
$mojangUsername->touch('last_pulled_at');
}
if (!$mojangUsername->save()) {
throw new ThisShouldNotHappenException('Cannot save mojang username');
}
}
}
protected function createMojangApi(): MojangApi {
return new MojangApi();
}
}

View File

@ -13,12 +13,9 @@
"league/oauth2-server": "^4.1", "league/oauth2-server": "^4.1",
"yiisoft/yii2-redis": "~2.0.0", "yiisoft/yii2-redis": "~2.0.0",
"guzzlehttp/guzzle": "^6.0.0", "guzzlehttp/guzzle": "^6.0.0",
"php-amqplib/php-amqplib": "^2.6.2",
"ely/yii2-tempmail-validator": "^2.0", "ely/yii2-tempmail-validator": "^2.0",
"emarref/jwt": "~1.0.3", "emarref/jwt": "~1.0.3",
"ely/amqp-controller": "dev-master#d7f8cdbc66c45e477c9c7d5d509bc0c1b11fd3ec",
"ely/email-renderer": "dev-master#8aa2e71c5b3b8e4a726c3c090b2997030ba29f73", "ely/email-renderer": "dev-master#8aa2e71c5b3b8e4a726c3c090b2997030ba29f73",
"predis/predis": "^1.0",
"mito/yii2-sentry": "^1.0", "mito/yii2-sentry": "^1.0",
"spomky-labs/otphp": "^9.0.2", "spomky-labs/otphp": "^9.0.2",
"bacon/bacon-qr-code": "^1.0", "bacon/bacon-qr-code": "^1.0",
@ -26,8 +23,7 @@
"webmozart/assert": "^1.2.0", "webmozart/assert": "^1.2.0",
"goaop/framework": "~2.2.0", "goaop/framework": "~2.2.0",
"domnikl/statsd": "^2.6", "domnikl/statsd": "^2.6",
"yiisoft/yii2-queue": "~2.0.2", "yiisoft/yii2-queue": "~2.1.0"
"enqueue/amqp-lib": "^0.8.11"
}, },
"require-dev": { "require-dev": {
"yiisoft/yii2-debug": "*", "yiisoft/yii2-debug": "*",
@ -40,17 +36,14 @@
"mockery/mockery": "^1.0.0", "mockery/mockery": "^1.0.0",
"php-mock/php-mock-mockery": "^1.2.0", "php-mock/php-mock-mockery": "^1.2.0",
"friendsofphp/php-cs-fixer": "^2.11", "friendsofphp/php-cs-fixer": "^2.11",
"ely/php-code-style": "^0.1.0" "ely/php-code-style": "^0.1.0",
"predis/predis": "^1.1"
}, },
"repositories": [ "repositories": [
{ {
"type": "composer", "type": "composer",
"url": "https://asset-packagist.org" "url": "https://asset-packagist.org"
}, },
{
"type": "git",
"url": "git@gitlab.ely.by:elyby/amqp-controller.git"
},
{ {
"type": "git", "type": "git",
"url": "git@gitlab.ely.by:elyby/email-renderer.git" "url": "git@gitlab.ely.by:elyby/email-renderer.git"

426
composer.lock generated
View File

@ -1,10 +1,10 @@
{ {
"_readme": [ "_readme": [
"This file locks the dependencies of your project to a known state", "This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "63497214afa6b50f50d3bc80fe9eb269", "content-hash": "b576f6f9babd8e00a7fa6768d56c37e9",
"packages": [ "packages": [
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@ -153,7 +153,7 @@
"version": "v1.3.2", "version": "v1.3.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/bestiejs/punycode.js.git", "url": "git@github.com:bestiejs/punycode.js.git",
"reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3" "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3"
}, },
"dist": { "dist": {
@ -610,42 +610,6 @@
], ],
"time": "2017-11-15T23:40:40+00:00" "time": "2017-11-15T23:40:40+00:00"
}, },
{
"name": "ely/amqp-controller",
"version": "dev-master",
"source": {
"type": "git",
"url": "git@gitlab.ely.by:elyby/amqp-controller.git",
"reference": "d7f8cdbc66c45e477c9c7d5d509bc0c1b11fd3ec"
},
"require": {
"php-amqplib/php-amqplib": "^2.6"
},
"type": "library",
"autoload": {
"psr-4": {
"Ely\\Amqp\\": "src/"
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "Ely.by team",
"email": "team@ely.by"
},
{
"name": "ErickSkrauch",
"email": "erickskrauch@ely.by"
}
],
"homepage": "http://ely.by",
"keywords": [
""
],
"time": "2016-11-15T19:40:20+00:00"
},
{ {
"name": "ely/email-renderer", "name": "ely/email-renderer",
"version": "dev-master", "version": "dev-master",
@ -779,117 +743,6 @@
"description": "A JWT implementation", "description": "A JWT implementation",
"time": "2016-09-05T20:33:06+00:00" "time": "2016-09-05T20:33:06+00:00"
}, },
{
"name": "enqueue/amqp-lib",
"version": "0.8.21",
"source": {
"type": "git",
"url": "https://github.com/php-enqueue/amqp-lib.git",
"reference": "5a0da2f2eccb2ebda4d0b2526e1753c96cf0ef75"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-enqueue/amqp-lib/zipball/5a0da2f2eccb2ebda4d0b2526e1753c96cf0ef75",
"reference": "5a0da2f2eccb2ebda4d0b2526e1753c96cf0ef75",
"shasum": ""
},
"require": {
"enqueue/amqp-tools": "^0.8.5@dev",
"php": ">=5.6",
"php-amqplib/php-amqplib": "^2.7@dev",
"queue-interop/amqp-interop": "^0.7@dev",
"queue-interop/queue-interop": "^0.6@dev"
},
"require-dev": {
"enqueue/enqueue": "^0.8@dev",
"enqueue/null": "^0.8@dev",
"enqueue/test": "^0.8@dev",
"phpunit/phpunit": "~5.4.0",
"queue-interop/queue-spec": "^0.5.3@dev",
"symfony/config": "^2.8|^3|^4",
"symfony/dependency-injection": "^2.8|^3|^4"
},
"suggest": {
"enqueue/enqueue": "If you'd like to use advanced features like Client abstract layer or Symfony integration features"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.8.x-dev"
}
},
"autoload": {
"psr-4": {
"Enqueue\\AmqpLib\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Message Queue Amqp Transport",
"homepage": "https://enqueue.forma-pro.com/",
"keywords": [
"AMQP",
"messaging",
"queue"
],
"time": "2018-02-16T11:05:22+00:00"
},
{
"name": "enqueue/amqp-tools",
"version": "0.8.14",
"source": {
"type": "git",
"url": "https://github.com/php-enqueue/amqp-tools.git",
"reference": "f375dee4d8609fca565a80df1c0f238bf0fe774f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-enqueue/amqp-tools/zipball/f375dee4d8609fca565a80df1c0f238bf0fe774f",
"reference": "f375dee4d8609fca565a80df1c0f238bf0fe774f",
"shasum": ""
},
"require": {
"php": ">=5.6",
"queue-interop/amqp-interop": "^0.7@dev",
"queue-interop/queue-interop": "^0.6@dev"
},
"require-dev": {
"enqueue/null": "^0.8@dev",
"enqueue/test": "^0.8@dev",
"phpunit/phpunit": "~5.4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.8.x-dev"
}
},
"autoload": {
"psr-4": {
"Enqueue\\AmqpTools\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Message Queue Amqp Tools",
"homepage": "https://enqueue.forma-pro.com/",
"keywords": [
"AMQP",
"messaging",
"queue"
],
"time": "2018-01-10T12:00:35+00:00"
},
{ {
"name": "ezyang/htmlpurifier", "name": "ezyang/htmlpurifier",
"version": "v4.9.3", "version": "v4.9.3",
@ -1619,127 +1472,6 @@
], ],
"time": "2017-09-27T21:40:39+00:00" "time": "2017-09-27T21:40:39+00:00"
}, },
{
"name": "php-amqplib/php-amqplib",
"version": "v2.7.2",
"source": {
"type": "git",
"url": "https://github.com/php-amqplib/php-amqplib.git",
"reference": "dfd3694a86f1a7394d3693485259d4074a6ec79b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/dfd3694a86f1a7394d3693485259d4074a6ec79b",
"reference": "dfd3694a86f1a7394d3693485259d4074a6ec79b",
"shasum": ""
},
"require": {
"ext-bcmath": "*",
"ext-mbstring": "*",
"php": ">=5.3.0"
},
"replace": {
"videlalvaro/php-amqplib": "self.version"
},
"require-dev": {
"phpdocumentor/phpdocumentor": "^2.9",
"phpunit/phpunit": "^4.8",
"scrutinizer/ocular": "^1.1",
"squizlabs/php_codesniffer": "^2.5"
},
"suggest": {
"ext-sockets": "Use AMQPSocketConnection"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.7-dev"
}
},
"autoload": {
"psr-4": {
"PhpAmqpLib\\": "PhpAmqpLib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "Alvaro Videla",
"role": "Original Maintainer"
},
{
"name": "John Kelly",
"email": "johnmkelly86@gmail.com",
"role": "Maintainer"
},
{
"name": "Raúl Araya",
"email": "nubeiro@gmail.com",
"role": "Maintainer"
}
],
"description": "Formerly videlalvaro/php-amqplib. This library is a pure PHP implementation of the AMQP protocol. It's been tested against RabbitMQ.",
"homepage": "https://github.com/php-amqplib/php-amqplib/",
"keywords": [
"message",
"queue",
"rabbitmq"
],
"time": "2018-02-11T19:28:00+00:00"
},
{
"name": "predis/predis",
"version": "v1.1.1",
"source": {
"type": "git",
"url": "https://github.com/nrk/predis.git",
"reference": "f0210e38881631afeafb56ab43405a92cafd9fd1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nrk/predis/zipball/f0210e38881631afeafb56ab43405a92cafd9fd1",
"reference": "f0210e38881631afeafb56ab43405a92cafd9fd1",
"shasum": ""
},
"require": {
"php": ">=5.3.9"
},
"require-dev": {
"phpunit/phpunit": "~4.8"
},
"suggest": {
"ext-curl": "Allows access to Webdis when paired with phpiredis",
"ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol"
},
"type": "library",
"autoload": {
"psr-4": {
"Predis\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Daniele Alessandri",
"email": "suppakilla@gmail.com",
"homepage": "http://clorophilla.net"
}
],
"description": "Flexible and feature-complete Redis client for PHP and HHVM",
"homepage": "http://github.com/nrk/predis",
"keywords": [
"nosql",
"predis",
"redis"
],
"time": "2016-06-16T16:22:20+00:00"
},
{ {
"name": "psr/http-message", "name": "psr/http-message",
"version": "1.0.1", "version": "1.0.1",
@ -1790,87 +1522,6 @@
], ],
"time": "2016-08-06T14:39:51+00:00" "time": "2016-08-06T14:39:51+00:00"
}, },
{
"name": "queue-interop/amqp-interop",
"version": "0.7.2",
"source": {
"type": "git",
"url": "https://github.com/queue-interop/amqp-interop.git",
"reference": "03cfac42483d07ab45d1896a6a2e1d873a216bba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/queue-interop/amqp-interop/zipball/03cfac42483d07ab45d1896a6a2e1d873a216bba",
"reference": "03cfac42483d07ab45d1896a6a2e1d873a216bba",
"shasum": ""
},
"require": {
"php": ">=5.5",
"queue-interop/queue-interop": "^0.6@dev"
},
"require-dev": {
"phpunit/phpunit": "~5.4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.7.x-dev"
}
},
"autoload": {
"psr-4": {
"Interop\\Amqp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"time": "2018-01-04T09:52:06+00:00"
},
{
"name": "queue-interop/queue-interop",
"version": "0.6.1",
"source": {
"type": "git",
"url": "https://github.com/queue-interop/queue-interop.git",
"reference": "38579005c0492c0275bbae31170edf30a7e740fa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/queue-interop/queue-interop/zipball/38579005c0492c0275bbae31170edf30a7e740fa",
"reference": "38579005c0492c0275bbae31170edf30a7e740fa",
"shasum": ""
},
"require": {
"php": ">=5.5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.6.x-dev"
}
},
"autoload": {
"psr-4": {
"Interop\\Queue\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Promoting the interoperability of MQs objects. Based on Java JMS",
"homepage": "https://github.com/queue-interop/queue-interop",
"keywords": [
"MQ",
"jms",
"message queue",
"messaging",
"queue"
],
"time": "2017-08-10T11:24:15+00:00"
},
{ {
"name": "ramsey/uuid", "name": "ramsey/uuid",
"version": "3.7.3", "version": "3.7.3",
@ -2701,24 +2352,25 @@
}, },
{ {
"name": "yiisoft/yii2-queue", "name": "yiisoft/yii2-queue",
"version": "2.0.2", "version": "2.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/yiisoft/yii2-queue.git", "url": "https://github.com/yiisoft/yii2-queue.git",
"reference": "8c2b337f7d9ea934c2affdfc21c9fb387d0a0773" "reference": "d04b4b3c932081200876a351cc6c3502e89e11b8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/yiisoft/yii2-queue/zipball/8c2b337f7d9ea934c2affdfc21c9fb387d0a0773", "url": "https://api.github.com/repos/yiisoft/yii2-queue/zipball/d04b4b3c932081200876a351cc6c3502e89e11b8",
"reference": "8c2b337f7d9ea934c2affdfc21c9fb387d0a0773", "reference": "d04b4b3c932081200876a351cc6c3502e89e11b8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=5.5.0", "php": ">=5.5.0",
"symfony/process": "*", "symfony/process": "*",
"yiisoft/yii2": "~2.0.13" "yiisoft/yii2": "~2.0.14"
}, },
"require-dev": { "require-dev": {
"aws/aws-sdk-php": ">=2.4",
"enqueue/amqp-lib": "^0.8", "enqueue/amqp-lib": "^0.8",
"jeremeamia/superclosure": "*", "jeremeamia/superclosure": "*",
"pda/pheanstalk": "*", "pda/pheanstalk": "*",
@ -2729,6 +2381,7 @@
"yiisoft/yii2-redis": "*" "yiisoft/yii2-redis": "*"
}, },
"suggest": { "suggest": {
"aws/aws-sdk-php": "Need for aws SQS.",
"enqueue/amqp-lib": "Need for AMQP interop queue.", "enqueue/amqp-lib": "Need for AMQP interop queue.",
"ext-gearman": "Need for Gearman queue.", "ext-gearman": "Need for Gearman queue.",
"ext-pcntl": "Need for process signals.", "ext-pcntl": "Need for process signals.",
@ -2752,7 +2405,8 @@
"yii\\queue\\file\\": "src/drivers/file", "yii\\queue\\file\\": "src/drivers/file",
"yii\\queue\\gearman\\": "src/drivers/gearman", "yii\\queue\\gearman\\": "src/drivers/gearman",
"yii\\queue\\redis\\": "src/drivers/redis", "yii\\queue\\redis\\": "src/drivers/redis",
"yii\\queue\\sync\\": "src/drivers/sync" "yii\\queue\\sync\\": "src/drivers/sync",
"yii\\queue\\sqs\\": "src/drivers/sqs"
} }
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
@ -2765,7 +2419,7 @@
"email": "zhuravljov@gmail.com" "email": "zhuravljov@gmail.com"
} }
], ],
"description": "Yii2 Queue Extension which supported DB, Redis, RabbitMQ, Beanstalk and Gearman", "description": "Yii2 Queue Extension which supported DB, Redis, RabbitMQ, Beanstalk, SQS and Gearman",
"keywords": [ "keywords": [
"async", "async",
"beanstalk", "beanstalk",
@ -2775,9 +2429,10 @@
"queue", "queue",
"rabbitmq", "rabbitmq",
"redis", "redis",
"sqs",
"yii" "yii"
], ],
"time": "2017-12-26T17:16:14+00:00" "time": "2018-05-23T21:04:57+00:00"
}, },
{ {
"name": "yiisoft/yii2-redis", "name": "yiisoft/yii2-redis",
@ -4657,6 +4312,56 @@
], ],
"time": "2018-01-06T05:45:45+00:00" "time": "2018-01-06T05:45:45+00:00"
}, },
{
"name": "predis/predis",
"version": "v1.1.1",
"source": {
"type": "git",
"url": "https://github.com/nrk/predis.git",
"reference": "f0210e38881631afeafb56ab43405a92cafd9fd1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nrk/predis/zipball/f0210e38881631afeafb56ab43405a92cafd9fd1",
"reference": "f0210e38881631afeafb56ab43405a92cafd9fd1",
"shasum": ""
},
"require": {
"php": ">=5.3.9"
},
"require-dev": {
"phpunit/phpunit": "~4.8"
},
"suggest": {
"ext-curl": "Allows access to Webdis when paired with phpiredis",
"ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol"
},
"type": "library",
"autoload": {
"psr-4": {
"Predis\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Daniele Alessandri",
"email": "suppakilla@gmail.com",
"homepage": "http://clorophilla.net"
}
],
"description": "Flexible and feature-complete Redis client for PHP and HHVM",
"homepage": "http://github.com/nrk/predis",
"keywords": [
"nosql",
"predis",
"redis"
],
"time": "2016-06-16T16:22:20+00:00"
},
{ {
"name": "sebastian/code-unit-reverse-lookup", "name": "sebastian/code-unit-reverse-lookup",
"version": "1.0.1", "version": "1.0.1",
@ -6026,7 +5731,6 @@
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": { "stability-flags": {
"roave/security-advisories": 20, "roave/security-advisories": 20,
"ely/amqp-controller": 20,
"ely/email-renderer": 20 "ely/email-renderer": 20
}, },
"prefer-stable": false, "prefer-stable": false,

View File

@ -1,98 +0,0 @@
<?php
namespace console\controllers;
use common\components\Mojang\Api as MojangApi;
use common\components\Mojang\exceptions\NoContentException;
use common\models\Account;
use common\models\amqp\AccountBanned;
use common\models\amqp\UsernameChanged;
use common\models\MojangUsername;
use Ely\Amqp\Builder\Configurator;
use GuzzleHttp\Exception\RequestException;
use Yii;
class AccountQueueController extends AmqpController {
public function getExchangeName() {
return 'events';
}
public function configure(Configurator $configurator) {
$configurator->exchange->topic()->durable();
$configurator->queue->name('accounts-accounts-events')->durable();
$configurator->bind->routingKey('accounts.username-changed')
->add()->routingKey('account.account-banned');
}
public function getRoutesMap() {
return [
'accounts.username-changed' => 'routeUsernameChanged',
'accounts.account-banned' => 'routeAccountBanned',
];
}
public function routeUsernameChanged(UsernameChanged $body): bool {
Yii::$app->statsd->inc('worker.account.usernameChanged.attempt');
$mojangApi = $this->createMojangApi();
try {
$response = $mojangApi->usernameToUUID($body->newUsername);
Yii::$app->statsd->inc('worker.account.usernameChanged.found');
} catch (NoContentException $e) {
$response = false;
Yii::$app->statsd->inc('worker.account.usernameChanged.not_found');
} catch (RequestException $e) {
return true;
}
/** @var MojangUsername|null $mojangUsername */
$mojangUsername = MojangUsername::findOne($body->newUsername);
if ($response === false) {
if ($mojangUsername !== null) {
$mojangUsername->delete();
}
} else {
if ($mojangUsername === null) {
$mojangUsername = new MojangUsername();
$mojangUsername->username = $response->name;
$mojangUsername->uuid = $response->id;
} else {
$mojangUsername->uuid = $response->id;
$mojangUsername->touch('last_pulled_at');
}
$mojangUsername->save();
}
return true;
}
public function routeAccountBanned(AccountBanned $body): bool {
$account = Account::findOne($body->accountId);
if ($account === null) {
Yii::warning('Cannot find banned account ' . $body->accountId . '. Skipping.');
return true;
}
foreach ($account->sessions as $authSession) {
$authSession->delete();
}
foreach ($account->minecraftAccessKeys as $key) {
$key->delete();
}
foreach ($account->oauthSessions as $oauthSession) {
$oauthSession->delete();
}
return true;
}
/**
* @return MojangApi
*/
protected function createMojangApi(): MojangApi {
return new MojangApi();
}
}

View File

@ -1,72 +0,0 @@
<?php
namespace console\controllers;
use Ely\Amqp\ControllerTrait;
use Exception;
use PhpAmqpLib\Message\AMQPMessage;
use Yii;
use yii\console\Controller;
use yii\db\Exception as YiiDbException;
use yii\helpers\ArrayHelper;
use yii\helpers\Inflector;
abstract class AmqpController extends Controller {
use ControllerTrait {
callback as _callback;
}
private $reconnected = false;
final public function actionIndex() {
$this->start();
}
public function getRoutesMap() {
return [];
}
/**
* Переопределяем метод callback, чтобы избержать логгирования в консоль ошибок,
* связанных с обвалом того или иного соединения. Это нормально, PHP рождён умирать,
* а не работать 24/7 в качестве демона.
*
* @param AMQPMessage $msg
* @throws YiiDbException
*/
public function callback(AMQPMessage $msg) {
try {
$this->_callback($msg);
} catch (YiiDbException $e) {
if ($this->reconnected || !$this->isRestorableException($e)) {
throw $e;
}
$this->reconnected = true;
Yii::$app->db->close();
Yii::$app->db->open();
$this->callback($msg);
}
$this->reconnected = false;
}
/**
* @inheritdoc
*/
protected function getConnection() {
return Yii::$app->amqp->getConnection();
}
/**
* @inheritdoc
*/
protected function buildRouteActionName($route) {
return ArrayHelper::getValue($this->getRoutesMap(), $route, 'route' . Inflector::camelize($route));
}
private function isRestorableException(Exception $e): bool {
return strpos($e->getMessage(), 'MySQL server has gone away') !== false
|| strcmp($e->getMessage(), 'Error while sending QUERY packet') !== false;
}
}

View File

@ -7,7 +7,18 @@ services:
depends_on: depends_on:
- db - db
- redis - redis
- rabbitmq volumes:
- ./:/var/www/html/
env_file: .env
worker:
build:
dockerfile: Dockerfile-dev
context: .
command: ['php', 'yii', 'queue/listen', '-v']
depends_on:
- db
- redis
volumes: volumes:
- ./:/var/www/html/ - ./:/var/www/html/
env_file: .env env_file: .env
@ -34,16 +45,6 @@ services:
volumes: volumes:
- ./data/redis:/data - ./data/redis:/data
rabbitmq:
image: rabbitmq:3.6-management
env_file: .env
environment:
- VIRTUAL_HOST=rabbitmq.account.ely.by.local
- VIRTUAL_PORT=15672
networks:
- default
- nginx-proxy
phpmyadmin: phpmyadmin:
build: ./docker/phpmyadmin build: ./docker/phpmyadmin
environment: environment:

View File

@ -2,14 +2,24 @@ version: '2'
services: services:
app: app:
image: registry.ely.by/elyby/accounts:latest image: registry.ely.by/elyby/accounts:latest
restart: always
depends_on:
- db
- redis
env_file: .env
worker:
image: registry.ely.by/elyby/accounts:latest
restart: always
command: ['php', 'yii', 'queue/listen', '-v']
depends_on: depends_on:
- db - db
- redis - redis
- rabbitmq
env_file: .env env_file: .env
web: web:
image: registry.ely.by/elyby/accounts-nginx:1.0.3 image: registry.ely.by/elyby/accounts-nginx:1.0.3
restart: always
volumes_from: volumes_from:
- app - app
links: links:
@ -21,19 +31,17 @@ services:
db: db:
build: ./docker/mariadb build: ./docker/mariadb
restart: always
env_file: .env env_file: .env
volumes: volumes:
- ./data/mysql:/var/lib/mysql - ./data/mysql:/var/lib/mysql
redis: redis:
image: redis:3.0-alpine image: redis:3.0-alpine
restart: always
volumes: volumes:
- ./data/redis:/data - ./data/redis:/data
rabbitmq:
image: rabbitmq:3.6
env_file: .env
networks: networks:
nginx-proxy: nginx-proxy:
external: external:

View File

@ -1,4 +1,4 @@
FROM phpmyadmin/phpmyadmin FROM phpmyadmin/phpmyadmin:4.7.9-1
RUN printf "\n\nrequire('./config.local.php');\n" >> /www/config.inc.php RUN printf "\n\nrequire('./config.local.php');\n" >> /www/config.inc.php

View File

@ -1,6 +0,0 @@
[program:account-queue-worker]
directory=/var/www/html
command=wait-for-it rabbitmq:5672 -- php yii account-queue
autostart=true
autorestart=true
priority=10

View File

@ -1,6 +0,0 @@
[program:queue-worker]
directory=/var/www/html
command=wait-for-it rabbitmq:5672 -- php yii queue/listen -v
autostart=true
autorestart=true
priority=10

View File

@ -4,7 +4,6 @@ modules:
- Filesystem - Filesystem
- Yii2 - Yii2
- tests\codeception\common\_support\FixtureHelper - tests\codeception\common\_support\FixtureHelper
- tests\codeception\common\_support\amqp\Helper
- tests\codeception\common\_support\Mockery - tests\codeception\common\_support\Mockery
- Redis - Redis
- Asserts - Asserts

View File

@ -3,7 +3,6 @@ modules:
enabled: enabled:
- Yii2: - Yii2:
part: [orm, email, fixtures] part: [orm, email, fixtures]
- tests\codeception\common\_support\amqp\Helper
- tests\codeception\common\_support\queue\CodeceptionQueueHelper - tests\codeception\common\_support\queue\CodeceptionQueueHelper
- tests\codeception\common\_support\Mockery - tests\codeception\common\_support\Mockery
config: config:

View File

@ -28,12 +28,6 @@ class ConfirmEmailFormTest extends TestCase {
/** @var Account $account */ /** @var Account $account */
$account = Account::findOne($fixture['account_id']); $account = Account::findOne($fixture['account_id']);
$this->assertEquals(Account::STATUS_ACTIVE, $account->status, 'user status changed to active'); $this->assertEquals(Account::STATUS_ACTIVE, $account->status, 'user status changed to active');
$message = $this->tester->grabLastSentAmqpMessage('events');
$body = json_decode($message->getBody(), true);
$this->assertEquals($account->id, $body['accountId']);
$this->assertEquals($account->username, $body['newUsername']);
$this->assertNull($body['oldUsername']);
} }
private function createModel($key) { private function createModel($key) {

View File

@ -32,19 +32,6 @@ class ChangeEmailFormTest extends TestCase {
/** @noinspection UnserializeExploitsInspection */ /** @noinspection UnserializeExploitsInspection */
$data = unserialize($newEmailConfirmationFixture['_data']); $data = unserialize($newEmailConfirmationFixture['_data']);
$this->assertEquals($data['newEmail'], $account->email); $this->assertEquals($data['newEmail'], $account->email);
$this->tester->canSeeAmqpMessageIsCreated('events');
}
public function testCreateTask() {
/** @var Account $account */
$account = Account::findOne($this->getAccountId());
$model = new ChangeEmailForm($account);
$model->createTask(1, 'test1@ely.by', 'test@ely.by');
$message = $this->tester->grabLastSentAmqpMessage('events');
$body = json_decode($message->getBody(), true);
$this->assertEquals(1, $body['accountId']);
$this->assertEquals('test1@ely.by', $body['newEmail']);
$this->assertEquals('test@ely.by', $body['oldEmail']);
} }
private function getAccountId() { private function getAccountId() {

View File

@ -4,6 +4,7 @@ namespace tests\codeception\api\unit\modules\accounts\models;
use api\modules\accounts\models\ChangeUsernameForm; use api\modules\accounts\models\ChangeUsernameForm;
use common\models\Account; use common\models\Account;
use common\models\UsernameHistory; use common\models\UsernameHistory;
use common\tasks\PullMojangUsername;
use tests\codeception\api\unit\TestCase; use tests\codeception\api\unit\TestCase;
use tests\codeception\common\fixtures\AccountFixture; use tests\codeception\common\fixtures\AccountFixture;
use tests\codeception\common\fixtures\UsernameHistoryFixture; use tests\codeception\common\fixtures\UsernameHistoryFixture;
@ -25,7 +26,10 @@ class ChangeUsernameFormTest extends TestCase {
$this->assertTrue($model->performAction()); $this->assertTrue($model->performAction());
$this->assertEquals('my_new_nickname', Account::findOne($this->getAccountId())->username); $this->assertEquals('my_new_nickname', Account::findOne($this->getAccountId())->username);
$this->assertInstanceOf(UsernameHistory::class, UsernameHistory::findOne(['username' => 'my_new_nickname'])); $this->assertInstanceOf(UsernameHistory::class, UsernameHistory::findOne(['username' => 'my_new_nickname']));
$this->tester->canSeeAmqpMessageIsCreated('events'); /** @var PullMojangUsername $job */
$job = $this->tester->grabLastQueuedJob();
$this->assertInstanceOf(PullMojangUsername::class, $job);
$this->assertSame($job->username, 'my_new_nickname');
} }
public function testPerformActionWithTheSameUsername() { public function testPerformActionWithTheSameUsername() {
@ -42,7 +46,7 @@ class ChangeUsernameFormTest extends TestCase {
'username' => $username, 'username' => $username,
['>=', 'applied_in', $callTime], ['>=', 'applied_in', $callTime],
]), 'no new UsernameHistory record, if we don\'t change username'); ]), 'no new UsernameHistory record, if we don\'t change username');
$this->tester->cantSeeAmqpMessageIsCreated('events'); $this->assertNull($this->tester->grabLastQueuedJob());
} }
public function testPerformActionWithChangeCase() { public function testPerformActionWithChangeCase() {
@ -58,17 +62,10 @@ class ChangeUsernameFormTest extends TestCase {
UsernameHistory::findOne(['username' => $newUsername]), UsernameHistory::findOne(['username' => $newUsername]),
'username should change, if we change case of some letters' 'username should change, if we change case of some letters'
); );
$this->tester->canSeeAmqpMessageIsCreated('events'); /** @var PullMojangUsername $job */
} $job = $this->tester->grabLastQueuedJob();
$this->assertInstanceOf(PullMojangUsername::class, $job);
public function testCreateTask() { $this->assertSame($job->username, $newUsername);
$model = new ChangeUsernameForm($this->getAccount());
$model->createEventTask(1, 'test1', 'test');
$message = $this->tester->grabLastSentAmqpMessage('events');
$body = json_decode($message->getBody(), true);
$this->assertEquals(1, $body['accountId']);
$this->assertEquals('test1', $body['newUsername']);
$this->assertEquals('test', $body['oldUsername']);
} }
private function getAccount(): Account { private function getAccount(): Account {

View File

@ -4,6 +4,7 @@ namespace tests\codeception\api\unit\modules\internal\models;
use api\modules\accounts\models\BanAccountForm; use api\modules\accounts\models\BanAccountForm;
use api\modules\internal\helpers\Error as E; use api\modules\internal\helpers\Error as E;
use common\models\Account; use common\models\Account;
use common\tasks\ClearAccountSessions;
use tests\codeception\api\unit\TestCase; use tests\codeception\api\unit\TestCase;
class BanFormTest extends TestCase { class BanFormTest extends TestCase {
@ -35,28 +36,10 @@ class BanFormTest extends TestCase {
$model = new BanAccountForm($account); $model = new BanAccountForm($account);
$this->assertTrue($model->performAction()); $this->assertTrue($model->performAction());
$this->assertEquals(Account::STATUS_BANNED, $account->status); $this->assertEquals(Account::STATUS_BANNED, $account->status);
$this->tester->canSeeAmqpMessageIsCreated('events'); /** @var ClearAccountSessions $job */
} $job = $this->tester->grabLastQueuedJob();
$this->assertInstanceOf(ClearAccountSessions::class, $job);
public function testCreateTask() { $this->assertSame($job->accountId, $account->id);
$account = new Account();
$account->id = 3;
$model = new BanAccountForm($account);
$model->createTask();
$message = json_decode($this->tester->grabLastSentAmqpMessage('events')->body, true);
$this->assertSame(3, $message['accountId']);
$this->assertSame(-1, $message['duration']);
$this->assertSame('', $message['message']);
$model = new BanAccountForm($account);
$model->duration = 123;
$model->message = 'test';
$model->createTask();
$message = json_decode($this->tester->grabLastSentAmqpMessage('events')->body, true);
$this->assertSame(3, $message['accountId']);
$this->assertSame(123, $message['duration']);
$this->assertSame('test', $message['message']);
} }
} }

View File

@ -36,17 +36,6 @@ class PardonFormTest extends TestCase {
$model = new PardonAccountForm($account); $model = new PardonAccountForm($account);
$this->assertTrue($model->performAction()); $this->assertTrue($model->performAction());
$this->assertEquals(Account::STATUS_ACTIVE, $account->status); $this->assertEquals(Account::STATUS_ACTIVE, $account->status);
$this->tester->canSeeAmqpMessageIsCreated('events');
}
public function testCreateTask() {
$account = new Account();
$account->id = 3;
$model = new PardonAccountForm($account);
$model->createTask();
$message = json_decode($this->tester->grabLastSentAmqpMessage('events')->body, true);
$this->assertSame(3, $message['accountId']);
} }
} }

View File

@ -1,91 +0,0 @@
<?php
namespace tests\codeception\common\_support\amqp;
use Codeception\Exception\ModuleException;
use Codeception\Module;
use Codeception\Module\Yii2;
class Helper extends Module {
/**
* Checks that message is created.
*
* ```php
* <?php
* // check that at least 1 message was created
* $I->seeAmqpMessageIsCreated();
*
* // check that only 3 messages were created
* $I->seeAmqpMessageIsCreated(3);
* ```
*
* @param string|null $exchange
* @param int|null $num
*/
public function seeAmqpMessageIsCreated($exchange = null, $num = null) {
if ($num === null) {
$this->assertNotEmpty($this->grabSentAmqpMessages($exchange), 'message were created');
return;
}
$this->assertCount(
$num,
$this->grabSentAmqpMessages($exchange),
'number of created messages is equal to ' . $num
);
}
/**
* Checks that no messages was created
*
* @param string|null $exchange
*/
public function dontSeeAmqpMessageIsCreated($exchange = null) {
$this->seeAmqpMessageIsCreated($exchange, 0);
}
/**
* Returns last sent message
*
* @param string|null $exchange
* @return \PhpAmqpLib\Message\AMQPMessage
*/
public function grabLastSentAmqpMessage($exchange = null) {
$this->seeAmqpMessageIsCreated();
$messages = $this->grabSentAmqpMessages($exchange);
return end($messages);
}
/**
* Returns array of all sent amqp messages.
* Each message is `\PhpAmqpLib\Message\AMQPMessage` instance.
* Useful to perform additional checks using `Asserts` module.
*
* @param string|null $exchange
* @return \PhpAmqpLib\Message\AMQPMessage[]
* @throws ModuleException
*/
public function grabSentAmqpMessages($exchange = null) {
$amqp = $this->grabComponent('amqp');
if (!$amqp instanceof TestComponent) {
throw new ModuleException($this, 'AMQP module is not mocked, can\'t test messages');
}
return $amqp->getSentMessages($exchange);
}
private function grabComponent(string $component) {
return $this->getYii2()->grabComponent($component);
}
private function getYii2(): Yii2 {
$yii2 = $this->getModule('Yii2');
if (!$yii2 instanceof Yii2) {
throw new ModuleException($this, 'Yii2 module must be configured');
}
return $yii2;
}
}

View File

@ -1,58 +0,0 @@
<?php
namespace tests\codeception\common\_support\amqp;
use common\components\RabbitMQ\Component;
use PhpAmqpLib\Connection\AbstractConnection;
class TestComponent extends Component {
private $sentMessages = [];
public function init() {
\yii\base\Component::init();
}
public function getConnection() {
/** @noinspection MagicMethodsValidityInspection */
/** @noinspection PhpMissingParentConstructorInspection */
return new class extends AbstractConnection {
public function __construct(
$user,
$password,
$vhost,
$insist,
$login_method,
$login_response,
$locale,
\PhpAmqpLib\Wire\IO\AbstractIO $io,
$heartbeat
) {
// ничего не делаем
}
};
}
public function sendToExchange($exchangeName, $routingKey, $message, $exchangeArgs = [], $publishArgs = []) {
$this->sentMessages[$exchangeName][] = $this->prepareMessage($message);
}
/**
* @param string|null $exchangeName
* @return \PhpAmqpLib\Message\AMQPMessage[]
*/
public function getSentMessages(string $exchangeName = null): array {
if ($exchangeName !== null) {
return $this->sentMessages[$exchangeName] ?? [];
}
$messages = [];
foreach ($this->sentMessages as $exchangeGroup) {
foreach ($exchangeGroup as $message) {
$messages[] = $message;
}
}
return $messages;
}
}

View File

@ -14,7 +14,12 @@ class CodeceptionQueueHelper extends Module {
*/ */
public function grabLastQueuedJob() { public function grabLastQueuedJob() {
$messages = $this->grabQueueJobs(); $messages = $this->grabQueueJobs();
return end($messages); $last = end($messages);
if ($last === false) {
return null;
}
return $last;
} }
/** /**

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace tests\codeception\common\fixtures;
use common\models\WebHookEvent;
use yii\test\ActiveFixture;
class WebHooksEventsFixture extends ActiveFixture {
public $modelClass = WebHookEvent::class;
public $dataFile = '@tests/codeception/common/fixtures/data/webhooks-events.php';
public $depends = [
WebHooksFixture::class,
];
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace tests\codeception\common\fixtures;
use common\models\WebHook;
use yii\test\ActiveFixture;
class WebHooksFixture extends ActiveFixture {
public $modelClass = WebHook::class;
public $dataFile = '@tests/codeception/common/fixtures/data/webhooks.php';
}

View File

@ -0,0 +1,11 @@
<?php
return [
[
'webhook_id' => 1,
'event_type' => 'account.edit',
],
[
'webhook_id' => 2,
'event_type' => 'account.edit',
],
];

View File

@ -0,0 +1,21 @@
<?php
return [
'webhook-with-secret' => [
'id' => 1,
'url' => 'http://localhost:80/webhooks/ely',
'secret' => 'my-secret',
'created_at' => 1531054333,
],
'webhook-without-secret' => [
'id' => 2,
'url' => 'http://localhost:81/webhooks/ely',
'secret' => null,
'created_at' => 1531054837,
],
'webhook-without-events' => [
'id' => 3,
'url' => 'http://localhost:82/webhooks/ely',
'secret' => null,
'created_at' => 1531054990,
],
];

View File

@ -3,6 +3,7 @@ modules:
enabled: enabled:
- Yii2: - Yii2:
part: [orm, email, fixtures] part: [orm, email, fixtures]
- tests\codeception\common\_support\queue\CodeceptionQueueHelper
- tests\codeception\common\_support\Mockery - tests\codeception\common\_support\Mockery
config: config:
Yii2: Yii2:

View File

@ -1,14 +1,20 @@
<?php <?php
declare(strict_types=1);
namespace tests\codeception\common\unit\models; namespace tests\codeception\common\unit\models;
use Codeception\Specify; use Codeception\Specify;
use common\components\UserPass; use common\components\UserPass;
use common\models\Account; use common\models\Account;
use common\tasks\CreateWebHooksDeliveries;
use tests\codeception\common\fixtures\MojangUsernameFixture; use tests\codeception\common\fixtures\MojangUsernameFixture;
use tests\codeception\common\unit\TestCase; use tests\codeception\common\unit\TestCase;
use Yii; use Yii;
use const common\LATEST_RULES_VERSION; use const common\LATEST_RULES_VERSION;
/**
* @covers \common\models\Account
*/
class AccountTest extends TestCase { class AccountTest extends TestCase {
use Specify; use Specify;
@ -119,4 +125,37 @@ class AccountTest extends TestCase {
$this->assertNull($account->getRegistrationIp()); $this->assertNull($account->getRegistrationIp());
} }
public function testAfterSaveInsertEvent() {
$account = new Account();
$account->afterSave(true, [
'username' => 'old-username',
]);
$this->assertNull($this->tester->grabLastQueuedJob());
}
public function testAfterSaveNotMeaningfulAttributes() {
$account = new Account();
$account->afterSave(false, [
'updatedAt' => time(),
]);
$this->assertNull($this->tester->grabLastQueuedJob());
}
public function testAfterSavePushEvent() {
$changedAttributes = [
'username' => 'old-username',
'email' => 'old-email@ely.by',
'uuid' => 'c3cc0121-fa87-4818-9c0e-4acb7f9a28c5',
'status' => 10,
'lang' => 'en',
];
$account = new Account();
$account->afterSave(false, $changedAttributes);
/** @var CreateWebHooksDeliveries $job */
$job = $this->tester->grabLastQueuedJob();
$this->assertInstanceOf(CreateWebHooksDeliveries::class, $job);
$this->assertSame($job->payloads['changedAttributes'], $changedAttributes);
}
} }

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace tests\codeception\common\unit\tasks;
use common\models\Account;
use common\tasks\ClearAccountSessions;
use tests\codeception\common\fixtures;
use tests\codeception\common\unit\TestCase;
use yii\queue\Queue;
/**
* @covers \common\tasks\ClearAccountSessions
*/
class ClearAccountSessionsTest extends TestCase {
public function _fixtures() {
return [
'accounts' => fixtures\AccountFixture::class,
'oauthSessions' => fixtures\OauthSessionFixture::class,
'minecraftAccessKeys' => fixtures\MinecraftAccessKeyFixture::class,
'authSessions' => fixtures\AccountSessionFixture::class,
];
}
public function testCreateFromAccount() {
$account = new Account();
$account->id = 123;
$task = ClearAccountSessions::createFromAccount($account);
$this->assertSame(123, $task->accountId);
}
public function testExecute() {
/** @var \common\models\Account $bannedAccount */
$bannedAccount = $this->tester->grabFixture('accounts', 'banned-account');
$task = new ClearAccountSessions();
$task->accountId = $bannedAccount->id;
$task->execute(mock(Queue::class));
$this->assertEmpty($bannedAccount->sessions);
$this->assertEmpty($bannedAccount->minecraftAccessKeys);
$this->assertEmpty($bannedAccount->oauthSessions);
}
}

View File

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace tests\codeception\common\unit\tasks;
use common\models\Account;
use common\tasks\CreateWebHooksDeliveries;
use common\tasks\DeliveryWebHook;
use tests\codeception\common\fixtures;
use tests\codeception\common\unit\TestCase;
use yii\queue\Queue;
/**
* @covers \common\tasks\CreateWebHooksDeliveries
*/
class CreateWebHooksDeliveriesTest extends TestCase {
public function _fixtures(): array {
return [
'webhooks' => fixtures\WebHooksFixture::class,
'webhooksEvents' => fixtures\WebHooksEventsFixture::class,
];
}
public function testCreateAccountEdit() {
$account = new Account();
$account->id = 123;
$account->username = 'mock-username';
$account->uuid = 'afc8dc7a-4bbf-4d3a-8699-68890088cf84';
$account->email = 'mock@ely.by';
$account->lang = 'en';
$account->status = Account::STATUS_ACTIVE;
$account->created_at = 1531008814;
$changedAttributes = [
'username' => 'old-username',
'uuid' => 'e05d33e9-ff91-4d26-9f5c-8250f802a87a',
'email' => 'old-email@ely.by',
'status' => 0,
];
$result = CreateWebHooksDeliveries::createAccountEdit($account, $changedAttributes);
$this->assertInstanceOf(CreateWebHooksDeliveries::class, $result);
$this->assertSame('account.edit', $result->type);
$this->assertArraySubset([
'id' => 123,
'uuid' => 'afc8dc7a-4bbf-4d3a-8699-68890088cf84',
'username' => 'mock-username',
'email' => 'mock@ely.by',
'lang' => 'en',
'isActive' => true,
'registered' => '2018-07-08T00:13:34+00:00',
'changedAttributes' => $changedAttributes,
], $result->payloads);
}
public function testExecute() {
$task = new CreateWebHooksDeliveries();
$task->type = 'account.edit';
$task->payloads = [
'id' => 123,
'uuid' => 'afc8dc7a-4bbf-4d3a-8699-68890088cf84',
'username' => 'mock-username',
'email' => 'mock@ely.by',
'lang' => 'en',
'isActive' => true,
'registered' => '2018-07-08T00:13:34+00:00',
'changedAttributes' => [
'username' => 'old-username',
'uuid' => 'e05d33e9-ff91-4d26-9f5c-8250f802a87a',
'email' => 'old-email@ely.by',
'status' => 0,
],
];
$task->execute(mock(Queue::class));
/** @var DeliveryWebHook[] $tasks */
$tasks = $this->tester->grabQueueJobs();
$this->assertCount(2, $tasks);
$this->assertInstanceOf(DeliveryWebHook::class, $tasks[0]);
$this->assertSame($task->type, $tasks[0]->type);
$this->assertSame($task->payloads, $tasks[0]->payloads);
$this->assertSame('http://localhost:80/webhooks/ely', $tasks[0]->url);
$this->assertSame('my-secret', $tasks[0]->secret);
$this->assertInstanceOf(DeliveryWebHook::class, $tasks[1]);
$this->assertSame($task->type, $tasks[1]->type);
$this->assertSame($task->payloads, $tasks[1]->payloads);
$this->assertSame('http://localhost:81/webhooks/ely', $tasks[1]->url);
$this->assertNull($tasks[1]->secret);
}
}

View File

@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace tests\codeception\common\unit\tasks;
use common\tasks\DeliveryWebHook;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use tests\codeception\common\unit\TestCase;
use yii\queue\Queue;
/**
* @covers \common\tasks\DeliveryWebHook
*/
class DeliveryWebHookTest extends TestCase {
private $historyContainer = [];
/**
* @var Response|\GuzzleHttp\Exception\GuzzleException
*/
private $response;
public function testCanRetry() {
$task = new DeliveryWebHook();
$this->assertFalse($task->canRetry(1, new \Exception()));
$request = new Request('POST', 'http://localhost');
$this->assertTrue($task->canRetry(4, new ConnectException('', $request)));
$this->assertTrue($task->canRetry(4, new ServerException('', $request)));
$this->assertFalse($task->canRetry(5, new ConnectException('', $request)));
$this->assertFalse($task->canRetry(5, new ServerException('', $request)));
}
public function testExecuteSuccessDelivery() {
$this->response = new Response();
$task = $this->createMockedTask();
$task->type = 'account.edit';
$task->url = 'http://localhost:81/webhooks/ely';
$task->payloads = [
'key' => 'value',
'another' => 'value',
];
$task->execute(mock(Queue::class));
/** @var Request $request */
$request = $this->historyContainer[0]['request'];
$this->assertSame('http://localhost:81/webhooks/ely', (string)$request->getUri());
$this->assertStringStartsWith('Account-Ely-Hookshot/', $request->getHeaders()['User-Agent'][0]);
$this->assertSame('account.edit', $request->getHeaders()['X-Ely-Accounts-Event'][0]);
$this->assertSame('application/x-www-form-urlencoded', $request->getHeaders()['Content-Type'][0]);
$this->assertArrayNotHasKey('X-Hub-Signature', $request->getHeaders());
$this->assertEquals('key=value&another=value', (string)$request->getBody());
}
public function testExecuteSuccessDeliveryWithSignature() {
$this->response = new Response();
$task = $this->createMockedTask();
$task->type = 'account.edit';
$task->url = 'http://localhost:81/webhooks/ely';
$task->secret = 'secret';
$task->payloads = [
'key' => 'value',
'another' => 'value',
];
$task->execute(mock(Queue::class));
/** @var Request $request */
$request = $this->historyContainer[0]['request'];
$this->assertSame('http://localhost:81/webhooks/ely', (string)$request->getUri());
$this->assertStringStartsWith('Account-Ely-Hookshot/', $request->getHeaders()['User-Agent'][0]);
$this->assertSame('account.edit', $request->getHeaders()['X-Ely-Accounts-Event'][0]);
$this->assertSame('application/x-www-form-urlencoded', $request->getHeaders()['Content-Type'][0]);
$this->assertSame('sha1=3c0b1eef564b2d3a5e9c0f2a8302b1b42b3d4784', $request->getHeaders()['X-Hub-Signature'][0]);
$this->assertEquals('key=value&another=value', (string)$request->getBody());
}
public function testExecuteHandleClientException() {
$this->response = new Response(403);
$task = $this->createMockedTask();
$task->type = 'account.edit';
$task->url = 'http://localhost:81/webhooks/ely';
$task->secret = 'secret';
$task->payloads = [
'key' => 'value',
'another' => 'value',
];
$task->execute(mock(Queue::class));
}
/**
* @expectedException \GuzzleHttp\Exception\ServerException
*/
public function testExecuteUnhandledException() {
$this->response = new Response(502);
$task = $this->createMockedTask();
$task->type = 'account.edit';
$task->url = 'http://localhost:81/webhooks/ely';
$task->secret = 'secret';
$task->payloads = [
'key' => 'value',
'another' => 'value',
];
$task->execute(mock(Queue::class));
}
private function createMockedTask(): DeliveryWebHook {
$container = &$this->historyContainer;
$response = $this->response;
return new class ($container, $response) extends DeliveryWebHook {
private $historyContainer;
private $response;
public function __construct(array &$historyContainer, $response) {
$this->historyContainer = &$historyContainer;
$this->response = $response;
}
protected function createStack(): HandlerStack {
$stack = parent::createStack();
$stack->setHandler(new MockHandler([$this->response]));
$stack->push(Middleware::history($this->historyContainer));
return $stack;
}
};
}
}

View File

@ -1,30 +1,32 @@
<?php <?php
namespace codeception\console\unit\controllers; declare(strict_types=1);
namespace tests\codeception\common\unit\tasks;
use common\components\Mojang\Api; use common\components\Mojang\Api;
use common\components\Mojang\exceptions\NoContentException; use common\components\Mojang\exceptions\NoContentException;
use common\components\Mojang\response\UsernameToUUIDResponse; use common\components\Mojang\response\UsernameToUUIDResponse;
use common\models\amqp\AccountBanned; use common\models\Account;
use common\models\amqp\UsernameChanged;
use common\models\MojangUsername; use common\models\MojangUsername;
use console\controllers\AccountQueueController; use common\tasks\PullMojangUsername;
use tests\codeception\common\fixtures\AccountFixture;
use tests\codeception\common\fixtures\MojangUsernameFixture; use tests\codeception\common\fixtures\MojangUsernameFixture;
use tests\codeception\console\unit\TestCase; use tests\codeception\common\unit\TestCase;
use Yii; use yii\queue\Queue;
class AccountQueueControllerTest extends TestCase {
/** /**
* @var AccountQueueController * @covers \common\tasks\PullMojangUsername
*/ */
private $controller; class PullMojangUsernameTest extends TestCase {
private $expectedResponse; private $expectedResponse;
/**
* @var PullMojangUsername
*/
private $task;
public function _fixtures() { public function _fixtures() {
return [ return [
'accounts' => AccountFixture::class,
'mojangUsernames' => MojangUsernameFixture::class, 'mojangUsernames' => MojangUsernameFixture::class,
]; ];
} }
@ -32,10 +34,9 @@ class AccountQueueControllerTest extends TestCase {
public function _before() { public function _before() {
parent::_before(); parent::_before();
/** @var AccountQueueController|\PHPUnit_Framework_MockObject_MockObject $controller */ /** @var PullMojangUsername|\PHPUnit_Framework_MockObject_MockObject $task */
$controller = $this->getMockBuilder(AccountQueueController::class) $task = $this->getMockBuilder(PullMojangUsername::class)
->setMethods(['createMojangApi']) ->setMethods(['createMojangApi'])
->setConstructorArgs(['account-queue', Yii::$app])
->getMock(); ->getMock();
/** @var Api|\PHPUnit_Framework_MockObject_MockObject $apiMock */ /** @var Api|\PHPUnit_Framework_MockObject_MockObject $apiMock */
@ -54,30 +55,31 @@ class AccountQueueControllerTest extends TestCase {
return $this->expectedResponse; return $this->expectedResponse;
}); });
$controller $task
->expects($this->any()) ->expects($this->any())
->method('createMojangApi') ->method('createMojangApi')
->willReturn($apiMock); ->willReturn($apiMock);
$this->controller = $controller; $this->task = $task;
} }
public function testRouteUsernameChangedUsernameExists() { public function testCreateFromAccount() {
$account = new Account();
$account->username = 'find-me';
$result = PullMojangUsername::createFromAccount($account);
$this->assertSame('find-me', $result->username);
}
public function testExecuteUsernameExists() {
$expectedResponse = new UsernameToUUIDResponse(); $expectedResponse = new UsernameToUUIDResponse();
$expectedResponse->id = '069a79f444e94726a5befca90e38aaf5'; $expectedResponse->id = '069a79f444e94726a5befca90e38aaf5';
$expectedResponse->name = 'Notch'; $expectedResponse->name = 'Notch';
$this->expectedResponse = $expectedResponse; $this->expectedResponse = $expectedResponse;
/** @var \common\models\Account $accountInfo */ /** @var \common\models\MojangUsername $mojangUsernameFixture */
$accountInfo = $this->tester->grabFixture('accounts', 'admin');
/** @var MojangUsername $mojangUsernameFixture */
$mojangUsernameFixture = $this->tester->grabFixture('mojangUsernames', 'Notch'); $mojangUsernameFixture = $this->tester->grabFixture('mojangUsernames', 'Notch');
$body = new UsernameChanged([ $this->task->username = 'Notch';
'accountId' => $accountInfo->id, $this->task->execute(mock(Queue::class));
'oldUsername' => $accountInfo->username,
'newUsername' => 'Notch',
]);
$this->controller->routeUsernameChanged($body);
/** @var MojangUsername|null $mojangUsername */ /** @var MojangUsername|null $mojangUsername */
$mojangUsername = MojangUsername::findOne('Notch'); $mojangUsername = MojangUsername::findOne('Notch');
$this->assertInstanceOf(MojangUsername::class, $mojangUsername); $this->assertInstanceOf(MojangUsername::class, $mojangUsername);
@ -85,81 +87,62 @@ class AccountQueueControllerTest extends TestCase {
$this->assertLessThanOrEqual(time(), $mojangUsername->last_pulled_at); $this->assertLessThanOrEqual(time(), $mojangUsername->last_pulled_at);
} }
public function testRouteUsernameChangedUsernameNotExists() { public function testExecuteChangedUsernameExists() {
$expectedResponse = new UsernameToUUIDResponse();
$expectedResponse->id = '069a79f444e94726a5befca90e38aaf5';
$expectedResponse->name = 'Notch';
$this->expectedResponse = $expectedResponse;
/** @var MojangUsername $mojangUsernameFixture */
$mojangUsernameFixture = $this->tester->grabFixture('mojangUsernames', 'Notch');
$this->task->username = 'Notch';
$this->task->execute(mock(Queue::class));
/** @var MojangUsername|null $mojangUsername */
$mojangUsername = MojangUsername::findOne('Notch');
$this->assertInstanceOf(MojangUsername::class, $mojangUsername);
$this->assertGreaterThan($mojangUsernameFixture->last_pulled_at, $mojangUsername->last_pulled_at);
$this->assertLessThanOrEqual(time(), $mojangUsername->last_pulled_at);
}
public function testExecuteChangedUsernameNotExists() {
$expectedResponse = new UsernameToUUIDResponse(); $expectedResponse = new UsernameToUUIDResponse();
$expectedResponse->id = '607153852b8c4909811f507ed8ee737f'; $expectedResponse->id = '607153852b8c4909811f507ed8ee737f';
$expectedResponse->name = 'Chest'; $expectedResponse->name = 'Chest';
$this->expectedResponse = $expectedResponse; $this->expectedResponse = $expectedResponse;
/** @var \common\models\Account $accountInfo */ $this->task->username = 'Chest';
$accountInfo = $this->tester->grabFixture('accounts', 'admin'); $this->task->execute(mock(Queue::class));
$body = new UsernameChanged([
'accountId' => $accountInfo['id'],
'oldUsername' => $accountInfo['username'],
'newUsername' => 'Chest',
]);
$this->controller->routeUsernameChanged($body);
/** @var MojangUsername|null $mojangUsername */ /** @var MojangUsername|null $mojangUsername */
$mojangUsername = MojangUsername::findOne('Chest'); $mojangUsername = MojangUsername::findOne('Chest');
$this->assertInstanceOf(MojangUsername::class, $mojangUsername); $this->assertInstanceOf(MojangUsername::class, $mojangUsername);
} }
public function testRouteUsernameChangedRemoveIfExistsNoMore() { public function testExecuteRemoveIfExistsNoMore() {
$this->expectedResponse = false; $this->expectedResponse = false;
/** @var \common\models\Account $accountInfo */
$accountInfo = $this->tester->grabFixture('accounts', 'admin');
$username = $this->tester->grabFixture('mojangUsernames', 'not-exists')['username']; $username = $this->tester->grabFixture('mojangUsernames', 'not-exists')['username'];
$body = new UsernameChanged([ $this->task->username = $username;
'accountId' => $accountInfo['id'], $this->task->execute(mock(Queue::class));
'oldUsername' => $accountInfo['username'],
'newUsername' => $username,
]);
$this->controller->routeUsernameChanged($body);
/** @var MojangUsername|null $mojangUsername */ /** @var MojangUsername|null $mojangUsername */
$mojangUsername = MojangUsername::findOne($username); $mojangUsername = MojangUsername::findOne($username);
$this->assertNull($mojangUsername); $this->assertNull($mojangUsername);
} }
public function testRouteUsernameChangedUuidUpdated() { public function testExecuteUuidUpdated() {
$expectedResponse = new UsernameToUUIDResponse(); $expectedResponse = new UsernameToUUIDResponse();
$expectedResponse->id = 'f498513ce8c84773be26ecfc7ed5185d'; $expectedResponse->id = 'f498513ce8c84773be26ecfc7ed5185d';
$expectedResponse->name = 'jeb'; $expectedResponse->name = 'jeb';
$this->expectedResponse = $expectedResponse; $this->expectedResponse = $expectedResponse;
/** @var \common\models\Account $accountInfo */
$accountInfo = $this->tester->grabFixture('accounts', 'admin');
/** @var MojangUsername $mojangInfo */ /** @var MojangUsername $mojangInfo */
$mojangInfo = $this->tester->grabFixture('mojangUsernames', 'uuid-changed'); $mojangInfo = $this->tester->grabFixture('mojangUsernames', 'uuid-changed');
$username = $mojangInfo['username']; $username = $mojangInfo['username'];
$body = new UsernameChanged([ $this->task->username = $username;
'accountId' => $accountInfo['id'], $this->task->execute(mock(Queue::class));
'oldUsername' => $accountInfo['username'],
'newUsername' => $username,
]);
$this->controller->routeUsernameChanged($body);
/** @var MojangUsername|null $mojangUsername */ /** @var MojangUsername|null $mojangUsername */
$mojangUsername = MojangUsername::findOne($username); $mojangUsername = MojangUsername::findOne($username);
$this->assertInstanceOf(MojangUsername::class, $mojangUsername); $this->assertInstanceOf(MojangUsername::class, $mojangUsername);
$this->assertNotEquals($mojangInfo->uuid, $mojangUsername->uuid); $this->assertNotEquals($mojangInfo->uuid, $mojangUsername->uuid);
} }
public function testRouteAccountBanned() {
/** @var \common\models\Account $bannedAccount */
$bannedAccount = $this->tester->grabFixture('accounts', 'banned-account');
$this->tester->haveFixtures([
'oauthSessions' => \tests\codeception\common\fixtures\OauthSessionFixture::class,
'minecraftAccessKeys' => \tests\codeception\common\fixtures\MinecraftAccessKeyFixture::class,
'authSessions' => \tests\codeception\common\fixtures\AccountSessionFixture::class,
]);
$body = new AccountBanned();
$body->accountId = $bannedAccount->id;
$this->controller->routeAccountBanned($body);
$this->assertEmpty($bannedAccount->sessions);
$this->assertEmpty($bannedAccount->minecraftAccessKeys);
$this->assertEmpty($bannedAccount->oauthSessions);
}
} }

View File

@ -20,9 +20,6 @@ return [
// Для тестов нам не сильно важна безопасность, а вот время прохождения тестов значительно сокращается // Для тестов нам не сильно важна безопасность, а вот время прохождения тестов значительно сокращается
'passwordHashCost' => 4, 'passwordHashCost' => 4,
], ],
'amqp' => [
'class' => tests\codeception\common\_support\amqp\TestComponent::class,
],
'queue' => [ 'queue' => [
'class' => tests\codeception\common\_support\queue\Queue::class, 'class' => tests\codeception\common\_support\queue\Queue::class,
], ],