diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c1d757f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +[*] +tab_width = 2 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..a52a73c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +templates/ +scaffolds/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..a52a73c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +templates/ +scaffolds/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..548c817 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,15 @@ +{ + "printWidth": 100, + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "overrides": [ + { + "files": "*.md", + "options": { + "printWidth": 100, + "proseWrap": "always" + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a1791be --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "cSpell.words": [ + "Cospend", + "projectid" + ] +} diff --git a/appinfo/info.xml b/appinfo/info.xml index 0ca9d79..154bd63 100755 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -25,4 +25,7 @@ It will use the default currency as the base, and will fill existing currencies autocurrency.page.index + + OCA\AutoCurrency\Cron\FetchCurrenciesJob + diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index b22865c..24fef0d 100755 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -9,20 +9,24 @@ use OCA\AutoCurrency\AppInfo\Application; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; use OCP\IRequest; +use OCP\BackgroundJob\IJobList; use OCP\Util; class PageController extends Controller { - public function __construct(IRequest $request) { - parent::__construct(Application::APP_ID, $request); - } + private IJobList $jobList; - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function index(): TemplateResponse { - Util::addScript(Application::APP_ID, 'autocurrency-main'); + public function __construct(IRequest $request, IJobList $jobList) { + parent::__construct(Application::APP_ID, $request); + $this->jobList = $jobList; + } - return new TemplateResponse(Application::APP_ID, 'main'); - } + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function index(): TemplateResponse { + Util::addScript(Application::APP_ID, 'autocurrency-main'); + // $this->jobList->add('OCA\AutoCurrency\BackgroundJob\FetchCurrenciesJob'); + return new TemplateResponse(Application::APP_ID, 'main'); + } } diff --git a/lib/Cron/FetchCurrenciesJob.php b/lib/Cron/FetchCurrenciesJob.php new file mode 100644 index 0000000..e7c1188 --- /dev/null +++ b/lib/Cron/FetchCurrenciesJob.php @@ -0,0 +1,27 @@ +service = $service; + $this->logger = $logger; + + // Run once a day + $this->setInterval(60); + // $this->setTimeSensitivity(\OCP\BackgroundJob\IJob::TIME_INSENSITIVE); + } + + protected function run($arguments) { + $this->logger->info('Running cron job for FetchCurrenciesTask - args: ' . json_encode($arguments)); + $this->service->doCron(); + } +} diff --git a/lib/Db/CospendProjectMapper.php b/lib/Db/CospendProjectMapper.php new file mode 100755 index 0000000..78f3400 --- /dev/null +++ b/lib/Db/CospendProjectMapper.php @@ -0,0 +1,47 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCA\AutoCurrency\Db; + +use OCA\Cospend\Db\Project; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * @template-extends QBMapper + */ +class CospendProjectMapper extends QBMapper { + public function __construct(IDBConnection $db) { + parent::__construct($db, 'cospend_projects', Project::class); + } + + /** + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException + * @throws DoesNotExistException + */ + public function find(int $id): Project { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('cospend_projects') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + return $this->findEntity($qb); + } + + /** + * @param string $projectId + * @return array + */ + public function findAll(): array { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('cospend_projects'); + return $this->findEntities($qb); + } +} diff --git a/lib/Db/Currency.php b/lib/Db/Currency.php new file mode 100755 index 0000000..4fee3c0 --- /dev/null +++ b/lib/Db/Currency.php @@ -0,0 +1,34 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCA\AutoCurrency\Db; + +use JsonSerializable; + +use OCP\AppFramework\Db\Entity; + +/** + * @method getId(): int + * @method getName(): string + * @method setName(string $name): void + * @method getExchangeRate(): string + * @method setExchangeRate(string $exchangeRate): void + * @method getProjectId(): string + * @method setProjectId(string $exchangeRate): void + */ +class Currency extends Entity implements JsonSerializable { + protected string $name = ''; + protected string $exchangeRate = ''; + protected string $projectid = ''; + + public function jsonSerialize(): array { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'exchange_rate' => $this->exchangeRate, + 'projectid' => $this->projectid, + ]; + } +} diff --git a/lib/Db/CurrencyMapper.php b/lib/Db/CurrencyMapper.php new file mode 100755 index 0000000..3eef885 --- /dev/null +++ b/lib/Db/CurrencyMapper.php @@ -0,0 +1,48 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCA\AutoCurrency\Db; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * @template-extends QBMapper + */ +class CurrencyMapper extends QBMapper { + public function __construct(IDBConnection $db) { + parent::__construct($db, 'cospend_currencies', Currency::class); + } + + /** + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException + * @throws DoesNotExistException + */ + public function find(int $id, string $projectId): Currency { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('cospend_currencies') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('projectid', $qb->createNamedParameter($projectId))); + return $this->findEntity($qb); + } + + /** + * @param string $projectId + * @return array + */ + public function findAll(string $projectId): array { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('cospend_currencies') + ->where($qb->expr()->eq('projectid', $qb->createNamedParameter($projectId))); + return $this->findEntities($qb); + } +} diff --git a/lib/Db/Note.php b/lib/Db/Note.php deleted file mode 100755 index 958afd9..0000000 --- a/lib/Db/Note.php +++ /dev/null @@ -1,33 +0,0 @@ - -// SPDX-License-Identifier: AGPL-3.0-or-later - -namespace OCA\AutoCurrency\Db; - -use JsonSerializable; - -use OCP\AppFramework\Db\Entity; - -/** - * @method getId(): int - * @method getTitle(): string - * @method setTitle(string $title): void - * @method getContent(): string - * @method setContent(string $content): void - * @method getUserId(): string - * @method setUserId(string $userId): void - */ -class Note extends Entity implements JsonSerializable { - protected string $title = ''; - protected string $content = ''; - protected string $userId = ''; - - public function jsonSerialize(): array { - return [ - 'id' => $this->id, - 'title' => $this->title, - 'content' => $this->content - ]; - } -} diff --git a/lib/Db/NoteMapper.php b/lib/Db/NoteMapper.php deleted file mode 100755 index 0767649..0000000 --- a/lib/Db/NoteMapper.php +++ /dev/null @@ -1,48 +0,0 @@ - -// SPDX-License-Identifier: AGPL-3.0-or-later - -namespace OCA\AutoCurrency\Db; - -use OCP\AppFramework\Db\DoesNotExistException; -use OCP\AppFramework\Db\Entity; -use OCP\AppFramework\Db\QBMapper; -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\IDBConnection; - -/** - * @template-extends QBMapper - */ -class NoteMapper extends QBMapper { - public function __construct(IDBConnection $db) { - parent::__construct($db, 'autocurrency', Note::class); - } - - /** - * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException - * @throws DoesNotExistException - */ - public function find(int $id, string $userId): Note { - /* @var $qb IQueryBuilder */ - $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from('autocurrency') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))); - return $this->findEntity($qb); - } - - /** - * @param string $userId - * @return array - */ - public function findAll(string $userId): array { - /* @var $qb IQueryBuilder */ - $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from('autocurrency') - ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))); - return $this->findEntities($qb); - } -} diff --git a/lib/Service/CurrencyNotFound.php b/lib/Service/CurrencyNotFound.php new file mode 100755 index 0000000..13e2db7 --- /dev/null +++ b/lib/Service/CurrencyNotFound.php @@ -0,0 +1,9 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCA\AutoCurrency\Service; + +class CurrencyNotFound extends \Exception { +} diff --git a/lib/Service/FetchCurrenciesService.php b/lib/Service/FetchCurrenciesService.php new file mode 100644 index 0000000..5a1e14b --- /dev/null +++ b/lib/Service/FetchCurrenciesService.php @@ -0,0 +1,103 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCA\AutoCurrency\Service; + +use Exception; + +use OCA\AutoCurrency\Service\CurrencyNotFound; +use OCA\AutoCurrency\Db\CospendProjectMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; + +use OCA\AutoCurrency\Db\Currency; +use OCA\AutoCurrency\Db\CurrencyMapper; +use OCP\ILogger; + +class FetchCurrenciesService { + private static $EXCHANGE_URL = 'https://api.exchangerate.host/latest?base={base}'; + private CurrencyMapper $currencyMapper; + private CospendProjectMapper $projectMapper; + private ILogger $logger; + + public function __construct(CurrencyMapper $currencyMapper, CospendProjectMapper $projectMapper, ILogger $logger) { + $this->currencyMapper = $currencyMapper; + $this->projectMapper = $projectMapper; + $this->logger = $logger; + } + + public function doCron(): void { + $projects = $this->projectMapper->findAll(); + $currencyMap = []; + + foreach ($projects as $project) { + $baseCurrency = $this->getCurrencyName($project->getCurrencyname()); + + if (isset($currencyMap[$baseCurrency])) { + $json = $currencyMap[$baseCurrency]; + } else { + // request currency exchange rates from the API + // $this->logger->info('Fetching exchange rates for base currency ' . $baseCurrency); + print('Fetching exchange rates for base currency ' . $baseCurrency); + $fp = fopen(str_replace('{base}', $baseCurrency, FetchCurrenciesService::$EXCHANGE_URL), 'r'); + $data = stream_get_contents($fp); + fclose($fp); + $json = json_decode($data, true); + // $this->logger->info('Fetched exchange rates for base currency: ' . json_encode($json)); + print('Fetched exchange rates for base currency: ' . json_encode($json)); + if ($json['success'] == false) { + // $this->logger->error(new \Error('Failed to fetch exchange rates for base currency ' . $baseCurrency)); + print(new \Error('Failed to fetch exchange rates for base currency ' . $baseCurrency)); + continue; + } + $currencyMap[$baseCurrency] = $json; + } + + $currencies = $this->findAll($project->id); + + foreach ($currencies as $currency) { + $cur = $this->getCurrencyName($currency->getName()); + $currency->setExchangeRate(1 / $json['rates'][$cur]); + // $this->logger->info('Setting exchange rate for currency ' . $cur . ' to ' . $json['rates'][$cur]); + print('Setting exchange rate for currency ' . $cur . ' to ' . (1 / $json['rates'][$cur])); + $this->currencyMapper->update($currency); + } + } + } + + private function getCurrencyName(string $name): string { + // find 3-letter currency code for the base currency + preg_match('/([A-Z]{3})/', $name, $matches); + + // $this->logger->info('Matches: ' . json_encode($matches)); + print('Matches: ' . json_encode($matches)); + + if (count($matches) === 2) { + $name = $matches[1]; + } + + return $name; + } + + /** + * @return list + */ + public function findAll(string $projectId): array { + return $this->currencyMapper->findAll($projectId); + } + + /** + * @return never + */ + private function handleException(Exception $e) { + if ($e instanceof DoesNotExistException || + $e instanceof MultipleObjectsReturnedException) { + // TODO determine type + throw new CurrencyNotFound($e->getMessage()); + } else { + throw $e; + } + } +} diff --git a/lib/Service/NoteNotFound.php b/lib/Service/ProjectNotFound.php similarity index 80% rename from lib/Service/NoteNotFound.php rename to lib/Service/ProjectNotFound.php index e952378..fa81f9e 100755 --- a/lib/Service/NoteNotFound.php +++ b/lib/Service/ProjectNotFound.php @@ -5,5 +5,5 @@ declare(strict_types=1); namespace OCA\AutoCurrency\Service; -class NoteNotFound extends \Exception { +class ProjectNotFound extends \Exception { }