mirror of
https://github.com/wavelog/wavelog
synced 2026-08-13 18:41:16 -04:00
483 lines
17 KiB
PHP
483 lines
17 KiB
PHP
<?php
|
|
|
|
if (!defined('BASEPATH')) exit('No direct script access allowed');
|
|
|
|
require_once __DIR__ . '/Api_v2_resource.php';
|
|
// DXCC lookups live in the standalone Wavelog\Dxcc namespace (same source the v1
|
|
// API uses); pull it in and reference it fully-qualified below.
|
|
require_once APPPATH . '../src/Dxcc/Dxcc.php';
|
|
|
|
/**
|
|
* API v2 - Lookup resource (read-only)
|
|
*
|
|
* A "look something up" endpoint for a single callsign or a single gridsquare,
|
|
* scoped to the token owner's own logbook.
|
|
*
|
|
* Callsign lookup — resolves a callsign to DXCC data plus the owner's
|
|
* worked/confirmed status, LoTW membership and optional callbook data (v2
|
|
* equivalent of the v1 lookup / private_lookup endpoints; used by FlexRadio
|
|
* overlay, DXClusterAPI, ...). The ?detail= parameter selects the depth:
|
|
* - basic: DXCC + Lat/Long/continent plus the owner's grid/name if the call
|
|
* was worked before. No per-band/mode history. (v1 `lookup`, default.)
|
|
* - full: additionally per-band/mode worked & confirmed flags, DXCC
|
|
* confirmation state and (opt-in) callbook data. (v1 `private_lookup`.)
|
|
*
|
|
* Grid lookup (?grid=) — whether a gridsquare is worked/confirmed in the owner's
|
|
* logbook (v2 equivalent of v1 logbook_check_grid). The special value
|
|
* ?grid=all returns every worked gridsquare instead (v1 logbook_get_worked_grids).
|
|
*
|
|
* Routes:
|
|
* GET /api/v2/lookup?callsign=.. callsign lookup
|
|
* GET /api/v2/lookup?grid=.. grid worked/confirmed check
|
|
* GET /api/v2/lookup?grid=all list of all worked gridsquares
|
|
*
|
|
* Scope: lookup:read
|
|
*/
|
|
class Lookup_resource extends Api_v2_resource {
|
|
|
|
/** Token scope of this resource (see Api_v2_resource::required_scope()). */
|
|
protected $scope = 'lookup';
|
|
|
|
/** Registry label for this resource's scope (see scope_definitions()). */
|
|
protected static function scope_labels() {
|
|
return [
|
|
'read' => __('Look up callsigns and grids'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* GET /api/v2/lookup?callsign=... or ?grid=...
|
|
* `?callsign=` looks up a single callsign, `?grid=` a gridsquare's
|
|
* worked/confirmed status instead.
|
|
*/
|
|
public function index() {
|
|
$grid = $this->param('grid');
|
|
if ($grid !== null && $grid !== '') {
|
|
$this->do_grid_lookup($grid);
|
|
return;
|
|
}
|
|
|
|
$callsign = $this->param('callsign');
|
|
if ($callsign === null || $callsign === '') {
|
|
throw new Api_v2_exception(
|
|
'validation_error',
|
|
'Provide a callsign (?callsign=...) or a grid (?grid=...)',
|
|
400
|
|
);
|
|
}
|
|
$this->do_lookup($callsign);
|
|
}
|
|
|
|
/**
|
|
* Callsign lookup itself.
|
|
* Optional query: ?detail=full|basic (default basic), ?band=, ?mode=,
|
|
* ?callbook=true, ?station_ids=1,2 (default: all of the owner's stations).
|
|
*/
|
|
protected function do_lookup($callsign) {
|
|
$callsign = strtoupper(trim((string) $callsign));
|
|
if ($callsign === '') {
|
|
throw new Api_v2_exception('validation_error', 'Missing callsign', 400);
|
|
}
|
|
|
|
$detail = strtolower(trim((string) $this->param('detail', 'basic')));
|
|
if (!in_array($detail, ['full', 'basic'], true)) {
|
|
throw new Api_v2_exception(
|
|
'validation_error',
|
|
'Unknown detail "' . $detail . '". Allowed: full, basic',
|
|
400,
|
|
['allowed' => ['full', 'basic']]
|
|
);
|
|
}
|
|
|
|
$this->CI->load->model('logbook_model');
|
|
|
|
$station_ids = implode(', ', $this->resolve_station_ids('station_ids'));
|
|
|
|
$data = ($detail === 'basic')
|
|
? $this->lookup_basic($callsign, $station_ids)
|
|
: $this->lookup_full($callsign, $station_ids);
|
|
|
|
$this->CI->api_v2_response->respond($data, 200, ['detail' => $detail]);
|
|
}
|
|
|
|
/**
|
|
* Grid lookup (?grid=): whether the given gridsquare is worked/confirmed in
|
|
* the owner's logbook. Optional ?band=, ?cnfm= (qsl|lotw|eqsl) and
|
|
* ?logbook_id= filters. Result is one of "Not Found", "Found" (no cnfm
|
|
* requested), "Worked", "Confirmed". Replaces the v1 logbook_check_grid.
|
|
*/
|
|
protected function do_grid_lookup($grid) {
|
|
$grid = strtoupper(trim((string) $grid));
|
|
if ($grid === '') {
|
|
throw new Api_v2_exception('validation_error', 'Missing gridsquare', 400);
|
|
}
|
|
|
|
// "ALL" is not a valid gridsquare (2 letters + 2 digits), so it can
|
|
// safely double as the "give me every worked grid" selector.
|
|
if ($grid === 'ALL') {
|
|
$this->do_worked_grids();
|
|
return;
|
|
}
|
|
|
|
$band = $this->param('band');
|
|
$cnfm = $this->parse_cnfm();
|
|
|
|
$result = 'Not Found';
|
|
$locations = $this->grid_station_location_ids();
|
|
if (!empty($locations)) {
|
|
$this->CI->load->model('logbook_model');
|
|
$query = $this->CI->logbook_model->check_if_grid_worked_in_logbook($grid, $locations, $band, $cnfm, $this->lookup_operator());
|
|
if ($query->num_rows() === 0) {
|
|
$result = 'Not Found';
|
|
} elseif ($cnfm === null) {
|
|
$result = 'Found';
|
|
} else {
|
|
$values = [];
|
|
foreach ($query->result() as $row) {
|
|
$values[] = $row->gridorcnfm;
|
|
}
|
|
$result = in_array('Y', $values, true) ? 'Confirmed' : 'Worked';
|
|
}
|
|
}
|
|
|
|
$this->CI->api_v2_response->respond(['gridsquare' => $grid, 'result' => $result], 200, ['type' => 'grid']);
|
|
}
|
|
|
|
/**
|
|
* Grid list (?grid=all): every 4-character gridsquare worked in the owner's
|
|
* logbook, VUCC grids included. Optional ?band=, ?cnfm= (qsl|lotw|eqsl) and
|
|
* ?logbook_id= filters. Replaces the v1 logbook_get_worked_grids.
|
|
*/
|
|
protected function do_worked_grids() {
|
|
$band = $this->param('band');
|
|
$cnfm = $this->parse_cnfm();
|
|
|
|
$meta = ['type' => 'worked_grids', 'band' => $band, 'cnfm' => $cnfm];
|
|
|
|
// No station locations means no QSOs to look at — and an empty id list
|
|
// would produce an "IN ()" SQL syntax error further down.
|
|
$locations = $this->grid_station_location_ids();
|
|
if (empty($locations)) {
|
|
$this->CI->api_v2_response->respond(['grids' => [], 'count' => 0], 200, $meta);
|
|
return;
|
|
}
|
|
|
|
$this->CI->load->model('api_model');
|
|
$grids = $this->CI->api_model->get_grids_worked_in_logbook($locations, $band, $cnfm, $this->lookup_operator());
|
|
|
|
$this->CI->api_v2_response->respond(['grids' => $grids, 'count' => count($grids)], 200, $meta);
|
|
}
|
|
|
|
/**
|
|
* Station-location ids (as an array, the form check_if_grid_worked_in_logbook
|
|
* expects) the grid lookup runs against. Defaults to all of the owner's
|
|
* station locations; ?logbook_id= narrows it to a single owned logbook (403
|
|
* for a foreign id). Returns [] when the logbook has no linked locations.
|
|
*
|
|
* @return int[]
|
|
*/
|
|
protected function grid_station_location_ids() {
|
|
$logbook_id = $this->param('logbook_id');
|
|
if ($logbook_id !== null && $logbook_id !== '') {
|
|
if (!is_numeric($logbook_id)) {
|
|
throw new Api_v2_exception('validation_error', 'logbook_id must be numeric', 400);
|
|
}
|
|
$this->CI->load->model('logbooks_model');
|
|
if (!$this->CI->logbooks_model->logbook_id_belongs_to_user((int) $logbook_id, $this->user_id())) {
|
|
throw new Api_v2_exception('forbidden', 'logbook_id does not belong to this token', 403);
|
|
}
|
|
$locations = $this->CI->logbooks_model->list_logbook_relationships((int) $logbook_id);
|
|
return ($locations === [-1]) ? [] : $locations;
|
|
}
|
|
|
|
$this->CI->load->model('stations');
|
|
$ids = [];
|
|
$query = $this->CI->stations->all_of_user($this->user_id());
|
|
if ($query !== null) {
|
|
foreach ($query->result() as $row) {
|
|
$ids[] = (int) $row->station_id;
|
|
}
|
|
}
|
|
return $ids;
|
|
}
|
|
|
|
// --- Lookup variants ---------------------------------------------------
|
|
|
|
/**
|
|
* Reduced lookup: DXCC derivation plus the owner's grid/name when the call
|
|
* was worked before. No QSO history. Mirrors the v1 `lookup` endpoint used
|
|
* by the DXClusterAPI.
|
|
*/
|
|
protected function lookup_basic($callsign, $station_ids) {
|
|
$date = date('Y-m-d');
|
|
|
|
$return = [
|
|
'callsign' => $callsign,
|
|
'dxcc' => false,
|
|
'dxcc_id' => -1,
|
|
'dxcc_lat' => '',
|
|
'dxcc_long' => '',
|
|
'dxcc_cqz' => '',
|
|
'dxcc_flag' => '',
|
|
'cont' => '',
|
|
'name' => '',
|
|
'gridsquare' => '',
|
|
'location' => '',
|
|
'iota_ref' => '',
|
|
'state' => '',
|
|
'us_county' => '',
|
|
'qsl_manager' => '',
|
|
'bearing' => '',
|
|
'workedBefore' => false,
|
|
'lotw_member' => false,
|
|
'suffix_slash' => '',
|
|
];
|
|
|
|
$dxccobj = new \Wavelog\Dxcc\Dxcc();
|
|
$dxcc = $dxccobj->dxcc_lookup($callsign, $date);
|
|
$return['dxcc_id'] = $dxcc['adif'] ?? '';
|
|
$return['dxcc'] = $dxcc['entity'] ?? '';
|
|
$return['dxcc_lat'] = $dxcc['lat'] ?? '';
|
|
$return['dxcc_long'] = $dxcc['long'] ?? '';
|
|
$return['dxcc_cqz'] = $dxcc['cqz'] ?? '';
|
|
$return['cont'] = $dxcc['cont'] ?? '';
|
|
|
|
// Owner's own record for this call (no band/mode scoping). Skipped when
|
|
// the owner has no station locations: the empty id list would produce an
|
|
// "IN ()" syntax error, and there is nothing to look up anyway.
|
|
$hit = ($station_ids === '')
|
|
? null
|
|
: $this->CI->logbook_model->call_lookup_result($callsign, $station_ids, '', 'NO BAND', 'NO MODE', $this->lookup_operator());
|
|
if ($hit != null) {
|
|
$return['name'] = $hit->COL_NAME;
|
|
$return['gridsquare'] = $hit->COL_GRIDSQUARE;
|
|
$return['location'] = $hit->COL_QTH;
|
|
$return['iota_ref'] = $hit->COL_IOTA;
|
|
$return['qsl_manager'] = $hit->COL_QSL_VIA;
|
|
$return['state'] = $hit->COL_STATE;
|
|
$return['us_county'] = $hit->COL_CNTY;
|
|
$return['workedBefore'] = true;
|
|
if ($return['gridsquare'] != '') {
|
|
$return['latlng'] = $this->grid_to_latlng($return['gridsquare']);
|
|
}
|
|
}
|
|
|
|
if (($return['dxcc'] ?? '') != '') {
|
|
$this->CI->load->library('DxccFlag');
|
|
$return['dxcc_flag'] = $this->CI->dxccflag->get($return['dxcc_id']);
|
|
}
|
|
|
|
$lotw_days = $this->CI->logbook_model->check_last_lotw($callsign);
|
|
if ($lotw_days !== null) {
|
|
$return['lotw_member'] = $lotw_days;
|
|
}
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Full lookup: everything from basic plus per-band/mode worked & confirmed
|
|
* flags, DXCC confirmation state and optional callbook data. Mirrors the v1
|
|
* `private_lookup` endpoint.
|
|
*/
|
|
protected function lookup_full($callsign, $station_ids) {
|
|
$date = date('Y-m-d');
|
|
$band = (string) $this->param('band', 'NO_BAND');
|
|
$mode = (string) $this->param('mode', 'NO_MODE');
|
|
|
|
$user = $this->CI->user_model->get_by_id($this->user_id())->row();
|
|
$default_confirmation = $user->user_default_confirmation ?? '';
|
|
|
|
$return = [
|
|
'callsign' => $callsign,
|
|
'dxcc' => false,
|
|
'dxcc_id' => -1,
|
|
'dxcc_lat' => '',
|
|
'dxcc_long' => '',
|
|
'dxcc_cqz' => '',
|
|
'dxcc_flag' => '',
|
|
'cont' => '',
|
|
'name' => '',
|
|
'gridsquare' => '',
|
|
'location' => '',
|
|
'iota_ref' => '',
|
|
'state' => '',
|
|
'us_county' => '',
|
|
'qsl_manager' => '',
|
|
'bearing' => '',
|
|
'call_worked' => false,
|
|
'call_worked_band' => false,
|
|
'call_worked_band_mode' => false,
|
|
'lotw_member' => false,
|
|
'dxcc_confirmed_on_band' => false,
|
|
'dxcc_confirmed_on_band_mode' => false,
|
|
'dxcc_confirmed' => false,
|
|
'call_confirmed' => false,
|
|
'call_confirmed_band' => false,
|
|
'call_confirmed_band_mode' => false,
|
|
'suffix_slash' => '',
|
|
];
|
|
|
|
$dxccobj = new \Wavelog\Dxcc\Dxcc();
|
|
$dxcc = $dxccobj->dxcc_lookup($callsign, $date);
|
|
|
|
// A trailing "/XXX" may itself be a DXCC prefix (e.g. W/DL1ABC): resolve
|
|
// it, otherwise map the common portable/mobile suffixes to a label.
|
|
$last_slash_pos = strrpos($callsign, '/');
|
|
$suffix_dxcc = null;
|
|
if ($last_slash_pos !== false && $last_slash_pos > 4) {
|
|
$suffix = substr($callsign, $last_slash_pos + 1);
|
|
switch ($suffix) {
|
|
case 'P':
|
|
$return['suffix_slash'] = 'Portable';
|
|
break;
|
|
case 'M':
|
|
$return['suffix_slash'] = 'Mobile';
|
|
break;
|
|
case 'MM':
|
|
$return['suffix_slash'] = 'Maritime Mobile';
|
|
break;
|
|
default:
|
|
// Not a known suffix: likely a DXCC prefix override.
|
|
$suffix_dxcc = $dxccobj->dxcc_lookup($suffix, $date);
|
|
$return['suffix_slash'] = null;
|
|
}
|
|
}
|
|
|
|
if (isset($suffix_dxcc['call'])) {
|
|
$return['dxcc_id'] = $suffix_dxcc['adif'];
|
|
$return['dxcc'] = $suffix_dxcc['entity'];
|
|
$return['dxcc_lat'] = $suffix_dxcc['lat'];
|
|
$return['dxcc_long'] = $suffix_dxcc['long'];
|
|
$return['dxcc_cqz'] = $suffix_dxcc['cqz'];
|
|
$return['cont'] = $suffix_dxcc['cont'];
|
|
} else {
|
|
$return['dxcc_id'] = $dxcc['adif'] ?? '';
|
|
$return['dxcc'] = $dxcc['entity'] ?? '';
|
|
$return['dxcc_lat'] = $dxcc['lat'] ?? '';
|
|
$return['dxcc_long'] = $dxcc['long'] ?? '';
|
|
$return['dxcc_cqz'] = $dxcc['cqz'] ?? '';
|
|
$return['cont'] = $dxcc['cont'] ?? '';
|
|
}
|
|
|
|
// ITU zone from the DXCC entity; only added when the entity has one.
|
|
$entity = $this->CI->logbook_model->get_entity($return['dxcc_id']);
|
|
if (is_array($entity) && (($entity['ituz'] ?? 0) > 0)) {
|
|
$return['dxcc_ituz'] = (int) $entity['ituz'];
|
|
}
|
|
|
|
// Owner's own worked/confirmed status for this call. Skipped without any
|
|
// station locations (see lookup_basic()).
|
|
$hit = ($station_ids === '')
|
|
? null
|
|
: $this->CI->logbook_model->call_lookup_result($callsign, $station_ids, $default_confirmation, $band, $mode, $this->lookup_operator());
|
|
if ($hit != null) {
|
|
$return['name'] = $hit->COL_NAME;
|
|
$return['gridsquare'] = $hit->COL_GRIDSQUARE;
|
|
$return['location'] = $hit->COL_QTH;
|
|
$return['iota_ref'] = $hit->COL_IOTA;
|
|
$return['qsl_manager'] = $hit->COL_QSL_VIA;
|
|
$return['state'] = $hit->COL_STATE;
|
|
$return['us_county'] = $hit->COL_CNTY;
|
|
$return['dxcc_id'] = $hit->COL_DXCC;
|
|
$return['cont'] = $hit->COL_CONT;
|
|
$return['call_worked'] = true;
|
|
$return['call_worked_band'] = ($hit->CALL_WORKED_BAND == 1);
|
|
$return['call_worked_band_mode'] = ($hit->CALL_WORKED_BAND_MODE == 1);
|
|
$return['call_confirmed'] = ($hit->CALL_CNF == 1);
|
|
$return['call_confirmed_band'] = ($hit->CALL_CNF_BAND == 1);
|
|
$return['call_confirmed_band_mode'] = ($hit->CALL_CNF_BAND_MODE == 1);
|
|
if ($return['gridsquare'] != '') {
|
|
$return['latlng'] = $this->grid_to_latlng($return['gridsquare']);
|
|
}
|
|
}
|
|
|
|
if (($return['dxcc'] ?? '') != '') {
|
|
$this->CI->load->library('DxccFlag');
|
|
$return['dxcc_flag'] = $this->CI->dxccflag->get($return['dxcc_id']);
|
|
}
|
|
|
|
$lotw_days = $this->CI->logbook_model->check_last_lotw($callsign);
|
|
if ($lotw_days !== null) {
|
|
$return['lotw_member'] = $lotw_days;
|
|
}
|
|
|
|
if (($return['dxcc_id'] ?? '') != '') {
|
|
// The model returns [] for an empty station list; cast before the
|
|
// comparison, since in PHP an array is always "greater than" 0.
|
|
$return['dxcc_confirmed'] = ((int) $this->CI->logbook_model->check_if_dxcc_cnfmd_in_logbook_api($default_confirmation, $return['dxcc_id'], $station_ids, null, null, $this->lookup_operator()) > 0);
|
|
$return['dxcc_confirmed_on_band'] = ((int) $this->CI->logbook_model->check_if_dxcc_cnfmd_in_logbook_api($default_confirmation, $return['dxcc_id'], $station_ids, $band, null, $this->lookup_operator()) > 0);
|
|
$return['dxcc_confirmed_on_band_mode'] = ((int) $this->CI->logbook_model->check_if_dxcc_cnfmd_in_logbook_api($default_confirmation, $return['dxcc_id'], $station_ids, $band, $mode, $this->lookup_operator()) > 0);
|
|
}
|
|
|
|
// Callbook data is opt-in (external HTTP lookup).
|
|
if ($this->param('callbook') === 'true') {
|
|
$this->CI->load->library('callbook');
|
|
$callbook = $this->CI->logbook_model->loadCallBook($callsign, $this->CI->config->item('use_fullname'));
|
|
if ($callbook) {
|
|
$return['callbook'] = $callbook;
|
|
}
|
|
}
|
|
|
|
return $return;
|
|
}
|
|
|
|
// --- Helpers -----------------------------------------------------------
|
|
|
|
/**
|
|
* COL_OPERATOR this lookup is restricted to, or '' for no restriction.
|
|
*
|
|
* A lookup answers "have I worked this before" out of the logbook, and for a
|
|
* club token that logbook is the whole clubstation's. Without this the
|
|
* endpoint would hand a member the name, QTH and locator recorded in another
|
|
* operator's QSO - exactly the rows the QSO resource hides from it.
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function lookup_operator() {
|
|
return $this->is_restricted_club_member() ? (string) $this->operator_callsign() : '';
|
|
}
|
|
|
|
/**
|
|
* Parse and validate the ?cnfm= confirmation type, or null when absent.
|
|
*
|
|
* Both grid paths must validate this: check_if_grid_worked_in_logbook()
|
|
* switches on the value and falls back to selecting the gridsquare itself
|
|
* for anything it does not know. An unvalidated value would therefore be
|
|
* compared against gridsquare substrings instead of a confirmation flag and
|
|
* silently report "Worked" rather than failing.
|
|
*
|
|
* @return string|null 'qsl', 'lotw' or 'eqsl'.
|
|
* @throws Api_v2_exception 400 on an unknown value.
|
|
*/
|
|
protected function parse_cnfm() {
|
|
$allowed = ['qsl', 'lotw', 'eqsl'];
|
|
|
|
$cnfm = $this->param('cnfm');
|
|
if ($cnfm === null || $cnfm === '') {
|
|
return null;
|
|
}
|
|
|
|
$cnfm = strtolower(trim((string) $cnfm));
|
|
if (!in_array($cnfm, $allowed, true)) {
|
|
throw new Api_v2_exception(
|
|
'validation_error',
|
|
'Unknown cnfm "' . $cnfm . '". Allowed: ' . implode(', ', $allowed),
|
|
400,
|
|
['allowed' => $allowed]
|
|
);
|
|
}
|
|
|
|
return $cnfm;
|
|
}
|
|
|
|
/**
|
|
* Convert a Maidenhead locator to [lat, long] via the Qra library.
|
|
*/
|
|
protected function grid_to_latlng($grid) {
|
|
if (!$this->CI->load->is_loaded('Qra')) {
|
|
$this->CI->load->library('Qra');
|
|
}
|
|
return $this->CI->qra->qra2latlong($grid);
|
|
}
|
|
}
|