Skip to content

Commit

Permalink
chore: refactor services to eliminate code duplication (#27)
Browse files Browse the repository at this point in the history
* chore: refactor services to eliminate code duplication

* Apply fixes from StyleCI

* remove dupe line

---------

Co-authored-by: StyleCI Bot <[email protected]>
  • Loading branch information
imorland and StyleCIBot authored Sep 19, 2023
1 parent 77766fb commit 46e64e6
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 178 deletions.
2 changes: 1 addition & 1 deletion resources/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ fof-geoip:
service_ipstack_description: Use <b>https://ipstack.com</b> to make up to 10,000 lookups per month for free. Paid plans include connection info (ISP, organization) and threat level.

service_ipdata_label: IPData
service_ipdata_description: Use <b>https://ipdata.co</b> to make up to 1,500 lookups daily for free.
service_ipdata_description: Use <b>https://ipdata.co</b> to make up to 1,500 lookups daily for free. Paid plans for higher usage limits are also available.

service_ipapi_label: IP Api
service_ipapi_description: Use <b>http://ip-api.com</b> to make up to 45 lookups per minute for free. If you make more, your IP may get blacklisted, after which you can unban it at <b>http://ip-api.com/docs/unban</b>.
Expand Down
13 changes: 5 additions & 8 deletions src/Api/GeoIP.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
use Carbon\Carbon;
use Flarum\Settings\SettingsRepositoryInterface;
use FoF\GeoIP\IPInfo;
use FoF\GeoIP\Traits\HandlesGeoIPErrors;

class GeoIP
{
use HandlesGeoIPErrors;

public static $services = [
'ipapi' => Services\IPApi::class,
'ipdata' => Services\IPData::class,
Expand All @@ -26,14 +29,8 @@ class GeoIP

private $prefix = 'fof-geoip.services';

/**
* @var SettingsRepositoryInterface
*/
private $settings;

public function __construct(SettingsRepositoryInterface $settings)
public function __construct(protected SettingsRepositoryInterface $settings)
{
$this->settings = $settings;
}

/**
Expand Down Expand Up @@ -82,7 +79,7 @@ protected function checkErrors(): ?ServiceResponse
$lastErrorTime = $this->settings->get($timeKey);

if ($lastErrorTime && Carbon::createFromTimestamp($lastErrorTime)->isAfter(Carbon::now()->subHour())) {
return self::getFakeResponse($this->settings->get($errorKey));
return $this->handleGeoIPError($this->settings->get($errorKey));
} elseif ($lastErrorTime) {
$this->settings->delete($timeKey);
$this->settings->delete($errorKey);
Expand Down
77 changes: 77 additions & 0 deletions src/Api/Services/BaseGeoService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

/*
* This file is part of fof/geoip.
*
* Copyright (c) FriendsOfFlarum.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FoF\GeoIP\Api\Services;

use Flarum\Settings\SettingsRepositoryInterface;
use FoF\GeoIP\Api\ServiceInterface;
use FoF\GeoIP\Api\ServiceResponse;
use GuzzleHttp\Client;
use Psr\Log\LoggerInterface;

abstract class BaseGeoService implements ServiceInterface
{
/**
* @var Client
*/
protected $client;

protected $host;
protected $settingPrefix;

public function __construct(protected SettingsRepositoryInterface $settings, protected LoggerInterface $logger)
{
$this->client = new Client([
'base_uri' => $this->host,
'verify' => false,
]);
}

public function get(string $ip): ?ServiceResponse
{
$apiKey = $this->settings->get("{$this->settingPrefix}.access_key");

if ($this->requiresApiKey() && !$apiKey) {
$this->logger->error("No API key found for {$this->host}");

return null;
}

$res = $this->client->get($this->buildUrl($ip, $apiKey), $this->getRequestOptions($apiKey));

$body = json_decode($res->getBody());

if ($this->hasError($body)) {
$this->logger->error("Error detected in response from {$this->host}");

return $this->handleError($body);
}

$data = $this->parseResponse($body);

return $data;
}

protected function requiresApiKey(): bool
{
return true;
}

abstract protected function buildUrl(string $ip, ?string $apiKey): string;

abstract protected function getRequestOptions(?string $apiKey): array;

abstract protected function hasError(object $body): bool;

abstract protected function handleError(object $body): ?ServiceResponse;

abstract protected function parseResponse(object $body): ServiceResponse;
}
63 changes: 29 additions & 34 deletions src/Api/Services/IPApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,53 +11,48 @@

namespace FoF\GeoIP\Api\Services;

use Flarum\Settings\SettingsRepositoryInterface;
use FoF\GeoIP\Api\ServiceInterface;
use FoF\GeoIP\Api\GeoIP;
use FoF\GeoIP\Api\ServiceResponse;
use GuzzleHttp\Client;

class IPApi implements ServiceInterface
class IPApi extends BaseGeoService
{
/**
* @var SettingsRepositoryInterface
*/
protected $settings;
protected $host = 'http://ip-api.com';
protected $settingPrefix = 'fof-geoip.services.ipapi';

/**
* @var Client
*/
private $client;

public function __construct(SettingsRepositoryInterface $settings)
protected function buildUrl(string $ip, ?string $apiKey): string
{
$this->settings = $settings;
return "/json/{$ip}";
}

$this->client = new Client([
'base_uri' => 'http://ip-api.com',
]);
protected function requiresApiKey(): bool
{
return false;
}

/**
* @param string $ip
*
* @return ServiceResponse|null
*/
public function get(string $ip)
protected function getRequestOptions(?string $apiKey): array
{
$res = $this->client->get(
"/json/{$ip}",
['query' => [
return [
'http_errors' => false,
'delay' => 100,
'retries' => 3,
'query' => [
'fields' => 'status,message,countryCode,zip,isp,org',
]]
);
],
];
}

$body = json_decode($res->getBody());
protected function hasError(object $body): bool
{
return $body->status !== 'success';
}

if ($body->status != 'success') {
return (new ServiceResponse())
->setError($body->message);
}
protected function handleError(object $body): ?ServiceResponse
{
return GeoIP::setError('ipapi', $body->message ?? json_encode($body));
}

protected function parseResponse(object $body): ServiceResponse
{
return (new ServiceResponse())
->setCountryCode($body->countryCode)
->setZipCode($body->zip)
Expand Down
88 changes: 30 additions & 58 deletions src/Api/Services/IPData.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,86 +11,58 @@

namespace FoF\GeoIP\Api\Services;

use Flarum\Settings\SettingsRepositoryInterface;
use FoF\GeoIP\Api\GeoIP;
use FoF\GeoIP\Api\ServiceInterface;
use FoF\GeoIP\Api\ServiceResponse;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Str;

class IPData implements ServiceInterface
class IPData extends BaseGeoService
{
/**
* @var SettingsRepositoryInterface
*/
protected $settings;
protected $host = 'https://api.ipdata.co';
protected $settingPrefix = 'fof-geoip.services.ipdata';

/**
* @var Client
*/
private $client;

public function __construct(SettingsRepositoryInterface $settings)
protected function buildUrl(string $ip, ?string $apiKey): string
{
$this->settings = $settings;

$this->client = new Client([
'base_uri' => 'https://api.ipdata.co',
'verify' => false,
]);
return "/{$ip}";
}

/**
* @param string $ip
*
* @return ServiceResponse|null
*/
public function get(string $ip)
protected function getRequestOptions(?string $apiKey): array
{
$apiKey = $this->settings->get('fof-geoip.services.ipdata.access_key');

if (!$apiKey) {
return;
}

$res = null;

try {
$res = $this->client->get("/{$ip}", [
'query' => [
'fields' => 'country_code,postal,asn,threat',
'api-key' => $apiKey,
],
]);
} catch (RequestException $e) {
$body = json_decode($e->getResponse()->getBody());
$error = $body->message;

if (Str::startsWith($error, 'You have either exceeded your quota or that API key does not exist.')) {
return GeoIP::setError('ipdata', $error);
}
return [
'http_errors' => false,
'delay' => 100,
'retries' => 3,
'query' => [
'fields' => 'country_code,postal,asn,threat',
'api-key' => $apiKey,
],
];
}

return (new ServiceResponse())
->setError($body->message);
}
protected function hasError(object $body): bool
{
return isset($body->error);
}

$body = json_decode($res->getBody());
protected function handleError(object $body): ?ServiceResponse
{
return GeoIP::setError('ipdata', $body->error->message ?? json_encode($body));
}

$data = (new ServiceResponse())
protected function parseResponse(object $body): ServiceResponse
{
$response = (new ServiceResponse())
->setCountryCode($body->country_code)
->setZipCode($body->postal)
->setThreatLevel($body->threat->is_threat)
->setThreatType($body->threat->is_known_attacker ? 'attacker' : ($body->threat->is_known_abuser ? 'abuser' : null));

if (isset($body->asn->type)) {
if ($body->asn->type == 'isp') {
$data->setIsp($body->asn->name);
$response->setIsp($body->asn->name);
} else {
$data->setOrganization($body->asn->name);
$response->setOrganization($body->asn->name);
}
}

return $data;
return $response;
}
}
56 changes: 28 additions & 28 deletions src/Api/Services/IPLocation.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,45 +11,45 @@

namespace FoF\GeoIP\Api\Services;

use FoF\GeoIP\Api\ServiceInterface;
use FoF\GeoIP\Api\GeoIP;
use FoF\GeoIP\Api\ServiceResponse;
use GuzzleHttp\Client;

class IPLocation implements ServiceInterface
class IPLocation extends BaseGeoService
{
/**
* @var Client
*/
private Client $client;
protected $host = 'https://api.iplocation.net';
protected $settingPrefix = 'fof-geoip.services.iplocation';

public function __construct()
protected function buildUrl(string $ip, ?string $apiKey): string
{
$this->client = new Client([
'base_uri' => 'https://api.iplocation.net',
]);
return "/?ip={$ip}";
}

/**
* @param string $ip
*
* @return ServiceResponse|null
*/
public function get(string $ip)
protected function getRequestOptions(?string $apiKey): array
{
$res = $this->client->get(
'/',
['query' => [
'ip' => $ip,
]]
);
return [
'http_errors' => false,
'delay' => 100,
'retries' => 3,
];
}

$body = json_decode($res->getBody());
protected function requiresApiKey(): bool
{
return false;
}

if ($body->response_code != '200') {
return (new ServiceResponse())
->setError(sprintf('%s %s', $body->response_code, $body->response_message));
}
protected function hasError(object $body): bool
{
return $body->response_code !== '200';
}

protected function handleError(object $body): ?ServiceResponse
{
return GeoIP::setError('iplocation', sprintf('%s %s', $body->response_code, $body->response_message));
}

protected function parseResponse(object $body): ServiceResponse
{
return (new ServiceResponse())
->setCountryCode($body->country_code2)
->setIsp($body->isp);
Expand Down
Loading

0 comments on commit 46e64e6

Please sign in to comment.