Skip to content

Instantly share code, notes, and snippets.

@JC5
Created February 3, 2023 19:32
Show Gist options
  • Save JC5/3084d91c424b5162ea93049fc62aa946 to your computer and use it in GitHub Desktop.
Save JC5/3084d91c424b5162ea93049fc62aa946 to your computer and use it in GitHub Desktop.
<?php
/**
* autosave.php
* Copyright (c) 2020 [email protected]
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
bcscale(12);
/*
* INSTRUCTIONS FOR USE.
*
* Feel free to edit the code, read it and play with it. If you have questions feel free to ask them.
* Keep in mind that running this script is entirely AT YOUR OWN RISK with ZERO GUARANTEES.
*/
const FIREFLY_III_URL = 'https://firefly.example.com';
const FIREFLY_III_TOKEN = 'ey...';
/*
* HERE BE MONSTERS
*
* BELOW THIS LINE IS ACTUAL CODE (TM).
*/
// get and validate arguments
$arguments = getArguments($argv);
// action 1: download all user's rules:
if ('download-rules' === $arguments['action']) {
// if no file, exit:
if (!array_key_exists('file', $arguments)) {
messageAndExit('No file specified. Use --file=filename.json to specify a file.');
}
if ('' === $arguments['file']) {
messageAndExit('No file specified. Use --file=filename.json to specify a file.');
}
// download rules:
$rules = downloadRules();
// save to JSON file.
if (file_exists($arguments['file']) && array_key_exists('overwrite', $arguments)) {
message(sprintf('Will overwrite file "%s"', $arguments['file']));
unlink($arguments['file']);
}
if (file_exists($arguments['file']) && !array_key_exists('overwrite', $arguments)) {
messageAndExit(sprintf('Cowardly refuse to overwrite file "%s". Use "--overwrite".', $arguments['file']));
}
file_put_contents($arguments['file'], json_encode($rules, JSON_PRETTY_PRINT));
messageAndExit(sprintf('Saved all rules to "%s".', $arguments['file']));
}
// action 2: clear all existing rules
if ('clear-rules' === $arguments['action']) {
$isDryRun = array_key_exists('dry-run', $arguments);
$keepGroups = array_key_exists('keep-groups', $arguments);
clearRules($isDryRun, $keepGroups);
exit;
}
// action 3: upload new rule set
if ('upload-rules' === $arguments['action']) {
// overwrite or skip?
$overwrite = array_key_exists('overwrite', $arguments);
$skip = array_key_exists('skip', $arguments);
$clear = array_key_exists('clear', $arguments);
if ($overwrite && $skip) {
messageAndExit('You cannot use both --overwrite and --skip.');
}
if ($clear && $skip) {
messageAndExit('You cannot use both --clear and --skip.');
}
if ($clear && $overwrite) {
messageAndExit('You cannot use both --clear and --overwrite.');
}
// if no file, exit:
if (!array_key_exists('file', $arguments)) {
messageAndExit('No file specified. Use --file=filename.json to specify a file.');
}
if ('' === $arguments['file']) {
messageAndExit('No file specified. Use --file=filename.json to specify a file.');
}
if (!file_exists($arguments['file'])) {
messageAndExit(sprintf('File "%s" does not exist.', $arguments['file']));
}
// parse rules in file:
$content = file_get_contents($arguments['file']);
$json = json_decode($content, true, JSON_THROW_ON_ERROR);
if (false === $json || null === $json) {
messageAndExit('No rules found in file.');
}
if (0 === count($json)) {
messageAndExit('No rules found in file.');
}
$isDryRun = array_key_exists('dry-run', $arguments);
if (array_key_exists('clear', $arguments)) {
if ($isDryRun) {
message('Would have cleared all existing rules and rule groups first.');
}
if (!$isDryRun) {
message('Clearing all existing rules and rule groups first.');
}
clearRules($isDryRun, false);
}
// download rules
$existingRules = downloadRules();
$existingRuleGroups = downloadRuleGroups();
// loop json and action!
/** @var array $rule */
foreach ($json as $rule) {
// try to make a valid uploadable JSON string from the content in $rule
// code ignores problems, will just dump this in the user's lap.
$ruleExists = ruleExists($existingRules, $rule['title']);
$rule['rule_group_title'] = $rule['rule_group'] ?? 'Default group';
if ($isDryRun && $ruleExists && $overwrite) {
message(sprintf('Would have overwritten existing rule "%s".', $rule['title']));
continue;
}
if ($isDryRun && $ruleExists && $skip) {
message(sprintf('Would have skipped existing rule "%s".', $rule['title']));
continue;
}
if ($isDryRun && !$ruleExists) {
message(sprintf('Would have created new rule "%s".', $rule['title']));
continue;
}
if ($overwrite && $ruleExists) {
$existingRule = getExistingRule($existingRules, $rule['title']);
message(sprintf('Will overwrite existing rule #%d "%s".', $existingRule['id'], $rule['title']));
// delete rule
// submit rule
die('dont know how');
continue;
}
if ($skip && $ruleExists) {
$existingRule = getExistingRule($existingRules, $rule['title']);
message(sprintf('Cowardly refuse to overwrite existing rule #%d "%s".', $existingRule['id'], $rule['title']));
continue;
}
// see if rule group exists, create if it doesn't.
$ruleGroupExists = ruleGroupExists($existingRuleGroups, $rule['rule_group_title']);
if (!$ruleGroupExists) {
$group = [
'title' => $rule['rule_group_title'],
];
$result = postCurlRequest('/api/v1/rule_groups', $group);
message(sprintf('Stored new rule group "%s" under ID #%d', $group['title'], $result['data']['id']));
}
$result = postCurlRequest('/api/v1/rules', $rule);
message(sprintf('Stored new rule "%s" under ID #%d', $rule['title'], $result['data']['id']));
}
}
/**
* @param array $existingRules
* @param string $title
* @return array|null
*/
function getExistingRule(array $existingRules, string $title): ?array
{
foreach ($existingRules as $existingRule) {
if ($existingRule['title'] === $title) {
return $existingRule;
}
}
return null;
}
/**
* @param array $existingRules
* @param string $title
* @return bool
*/
function ruleExists(array $existingRules, string $title): bool
{
foreach ($existingRules as $existingRule) {
if ($existingRule['title'] === $title) {
message(sprintf('Rule "%s" already exists under id #%d in rule group "%s".', $title, $existingRule['id'], $existingRule['rule_group']));
return true;
}
}
message(sprintf('Rule "%s" does not exist yet', $title));
return false;
}
/**
* @param array $existing
* @param string $title
* @return bool
*/
function ruleGroupExists(array $existing, string $title): bool
{
foreach ($existing as $item) {
if ($existing['title'] === $title) {
message(sprintf('Rule group "%s" already exists under id #%d.', $title, $existing['id']));
return true;
}
}
message(sprintf('Rule group "%s" does not exist yet', $title));
return false;
}
/**
* @return array
*/
function downloadRuleGroups(): array
{
// loop over the rules:
$currentPage = 1;
$return = [];
$hasNextPage = true;
while ($hasNextPage) {
$current = getCurlRequest(sprintf('/api/v1/rule_groups?page=%d', $currentPage));
$totalPages = $current['meta']['pagination']['total_pages'];
$return = array_merge($return, $current['data']);
// set up the next run:
$hasNextPage = false;
if ($totalPages > $currentPage) {
$currentPage++;
$hasNextPage = true;
}
}
return $return;
}
/**
* Download all rules from server.
* @return array
*/
function downloadRules(): array
{
$result = [];
$currentPage = 1;
$hasNextPage = true;
$rules = [];
while ($hasNextPage) {
$current = getCurlRequest(sprintf('/api/v1/rules?page=%d', $currentPage));
$totalPages = $current['meta']['pagination']['total_pages'];
$rules = array_merge($rules, $current['data']);
// set up the next run:
$hasNextPage = false;
if ($totalPages > $currentPage) {
$currentPage++;
$hasNextPage = true;
}
}
// parse rules into a simpler array:
foreach ($rules as $rule) {
$newRule = [
'id' => $rule['id'],
'title' => $rule['attributes']['title'],
'rule_group' => $rule['attributes']['rule_group_title'],
'order' => (int)$rule['attributes']['order'],
'active' => $rule['attributes']['active'],
'strict' => $rule['attributes']['strict'],
'stop_processing' => $rule['attributes']['stop_processing'],
'trigger' => $rule['attributes']['trigger'],
'triggers' => [],
'actions' => [],
];
$index = 1;
foreach ($rule['attributes']['triggers'] as $trigger) {
$item = [
'type' => $trigger['type'],
'value' => $trigger['value'],
'order' => $index,
'active' => $trigger['active'],
'stop_processing' => $trigger['stop_processing'],
];
if ('' === $trigger['value'] || null === $trigger['value']) {
$item['value'] = null;
}
$newRule['triggers'][] = $item;
$index++;
}
$index = 1;
foreach ($rule['attributes']['actions'] as $action) {
$item = [
'type' => $action['type'],
'value' => $action['value'],
'order' => $index,
'active' => $action['active'],
'stop_processing' => $action['stop_processing'],
];
if ('' === $action['value'] || null === $action['value']) {
$item['value'] = null;
}
$index++;
$newRule['actions'][] = $item;
}
$result[] = $newRule;
}
return $result;
}
/**
* Clear all rules. Optionally keep rule groups.
*
* @param bool $isDryRun
* @param bool $keepGroups
* @return void
*/
function clearRules(bool $isDryRun, bool $keepGroups): void
{
$rules = downloadRules();
foreach ($rules as $rule) {
if ($isDryRun) {
message(sprintf('Would have deleted rule #%d, "%s"', $rule['id'], $rule['title']));
}
if (!$isDryRun) {
deleteCurlRequest(sprintf('/api/v1/rules/%d', $rule['id']));
message(sprintf('Deleted rule #%d, "%s"', $rule['id'], $rule['title']));
}
}
if (!$isDryRun && $keepGroups) {
message('Skipped deleting (now empty) rule groups.');
}
if ($isDryRun && $keepGroups) {
message('Would have skipped deleting (now empty) rule groups.');
}
if (!$keepGroups) {
// loop over rule groups:
$ruleGroups = downloadRuleGroups();
foreach ($ruleGroups as $ruleGroup) {
if ($isDryRun) {
message(sprintf('Would have deleted rule group #%d, "%s"', $ruleGroup['id'], $ruleGroup['attributes']['title']));
}
if (!$isDryRun) {
deleteCurlRequest(sprintf('/api/v1/rule_groups/%d', $ruleGroup['id']));
message(sprintf('Deleted rule group #%d, "%s"', $ruleGroup['id'], $ruleGroup['attributes']['title']));
}
}
}
}
/**
* @param array $arguments
* @return array
*/
function getArguments(array $arguments): array
{
$return = [];
$validActions = ['download-rules', 'clear-rules', 'upload-rules'];
if (count($arguments) < 2) {
infoAndExit();
}
$action = $arguments[1] ?? 'invalid';
if (!in_array($action, $validActions, true)) {
infoAndExit();
}
$return['action'] = $action;
foreach ($arguments as $arg) {
if (str_starts_with($arg, '--')) {
$parts = explode('=', $arg);
$return[substr($parts[0], 2)] = $parts[1] ?? '';
}
}
return $return;
}
/**
* @param string $message
*/
function message(string $message): void
{
echo $message.PHP_EOL;
}
function messageAndExit(string $message): never
{
message($message);
exit;
}
/**
* @return void
*/
function infoAndExit(): never
{
message('To use this application:');
message('');
message('php json-rules.php download-rules --file=some-file.json --overwrite');
message('php json-rules.php clear-rules --keep-groups --dry-run');
message('php json-rules.php upload-rules --file=some-file.json --overwrite --clear --dry-run');
message('php json-rules.php upload-rules --file=some-file.json --skip --clear --dry-run');
messageAndExit('');
}
/**
* @param string $url
*
* @return array
*/
function getCurlRequest(string $url): array
{
$ch = curl_init();
curl_setopt(
$ch, CURLOPT_HTTPHEADER,
[
'Content-Type: application/json',
'Accept: application/json',
sprintf('Authorization: Bearer %s', FIREFLY_III_TOKEN),
]
);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL, sprintf('%s%s', FIREFLY_III_URL, $url));
curl_setopt($ch, CURLOPT_TIMEOUT, 6);
// Execute
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (200 !== $httpCode) {
$error = curl_error($ch);
message(sprintf('Request %s returned with HTTP code %d.', $url, $httpCode));
message($error);
message((string)$result);
messageAndExit('');
}
$body = [];
try {
$body = json_decode($result, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
messageAndExit($e->getMessage());
}
return $body;
}
/**
* @param string $url
* @return void
*/
function deleteCurlRequest(string $url): void
{
$ch = curl_init();
curl_setopt(
$ch, CURLOPT_HTTPHEADER,
[
'Content-Type: application/json',
'Accept: application/json',
sprintf('Authorization: Bearer %s', FIREFLY_III_TOKEN),
]
);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL, sprintf('%s%s', FIREFLY_III_URL, $url));
curl_setopt($ch, CURLOPT_TIMEOUT, 6);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
// Execute
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (204 !== $httpCode) {
$error = curl_error($ch);
message(sprintf('Request %s returned with HTTP code %d.', $url, $httpCode));
message($error);
message((string)$result);
messageAndExit('');
}
}
/**
* @param string $url
* @param array $body
* @return array
* @throws JsonException
*/
function postCurlRequest(string $url, array $body): array
{
//message(sprintf('Going to POST %s', $url));
$ch = curl_init();
curl_setopt(
$ch, CURLOPT_HTTPHEADER,
[
'Content-Type: application/json',
'Accept: application/json',
sprintf('Authorization: Bearer %s', FIREFLY_III_TOKEN),
]
);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL, sprintf('%s%s', FIREFLY_III_URL, $url));
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_THROW_ON_ERROR));
// Execute
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (422 === $httpCode) {
$json = json_decode($result, true, 512, JSON_THROW_ON_ERROR);
message(sprintf('Validation error: %s', $json['message']));
foreach ($json['errors'] as $field => $errors) {
message(sprintf('Field "%s": ', $field));
foreach ($errors as $message) {
message(sprintf(' - %s', $message));
}
}
message('');
message('Please fix this in your JSON and try again.');
messageAndExit('');
}
if (200 !== $httpCode) {
$error = curl_error($ch);
message(sprintf('Request %s returned with HTTP code %d.', $url, $httpCode));
message($error);
message((string)$result);
messageAndExit('');
}
$body = [];
try {
$body = json_decode($result, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
messageAndExit($e->getMessage());
}
return $body;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment