refactor: replace custom CRM with Monica fork
Some checks failed
Build & Push Monica Image to Gitea Registry / build-and-push (push) Failing after 9s

This commit is contained in:
Kroonk
2026-05-22 16:19:55 +02:00
parent 38cc4c09ca
commit a213a1dba0
2215 changed files with 242567 additions and 6015 deletions

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Console\Commands;
use App\Models\Account\Account;
use Illuminate\Console\Command;
use App\Models\Instance\Statistic;
use Illuminate\Support\Facades\DB;
class CalculateStatistics extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:calculatestatistics';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Calculate general usage statistics';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$statistic = new Statistic;
$statistic->number_of_users = DB::table('users')->count();
$statistic->number_of_contacts = DB::table('contacts')->count();
$statistic->number_of_notes = DB::table('notes')->count();
$statistic->number_of_reminders = DB::table('reminders')->count();
$statistic->number_of_tasks = DB::table('tasks')->count();
$statistic->number_of_invitations_sent = DB::table('accounts')->sum('number_of_invitations_sent');
// number_of_accounts_with_more_than_one_user
$number_of_accounts_with_more_than_one_user = 0;
foreach (Account::all() as $account) {
if ($account->users()->count() > 1) {
$number_of_accounts_with_more_than_one_user = $number_of_accounts_with_more_than_one_user + 1;
}
}
$statistic->number_of_accounts_with_more_than_one_user = $number_of_accounts_with_more_than_one_user;
$statistic->number_of_import_jobs = DB::table('import_jobs')->count();
$statistic->number_of_tags = DB::table('tags')->count();
$statistic->number_of_activities = DB::table('activities')->count();
$statistic->number_of_addresses = DB::table('addresses')->count();
$statistic->number_of_api_calls = DB::table('api_usage')->count();
$statistic->number_of_calls = DB::table('calls')->count();
$statistic->number_of_contact_fields = DB::table('contact_fields')->count();
$statistic->number_of_contact_field_types = DB::table('contact_field_types')->count();
$statistic->number_of_debts = DB::table('debts')->count();
$statistic->number_of_entries = DB::table('entries')->count();
$statistic->number_of_gifts = DB::table('gifts')->count();
$statistic->number_of_oauth_access_tokens = DB::table('oauth_access_tokens')->count();
$statistic->number_of_oauth_clients = DB::table('oauth_clients')->count();
$statistic->number_of_relationships = DB::table('relationships')->count();
$statistic->number_of_subscriptions = DB::table('subscriptions')->count();
$statistic->number_of_conversations = DB::table('conversations')->count();
$statistic->number_of_messages = DB::table('messages')->count();
$statistic->save();
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Console\Commands;
use App\Models\User\SyncToken;
use Illuminate\Console\Command;
use App\Events\TokenDeleteEvent;
use App\Services\Instance\TokenClean;
use Illuminate\Support\Facades\Event;
class Clean extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:clean
{--dry-run : Do everything except actually clean monica instance.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clean monica instance';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
Event::listen(TokenDeleteEvent::class, function ($event) {
$this->handleTokenDelete($event->token);
});
app(TokenClean::class)->execute([
'dryrun' => (bool) $this->option('dry-run'),
]);
}
/**
* Handle TokenDeleteEvent event.
*
* @param SyncToken $token
*/
private function handleTokenDelete($token)
{
$this->info('Delete token '.$token->id.' - User '.$token->user_id.' - Type '.$token->name.' - timestamp '.$token->timestamp);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Console\Commands;
use App\Models\Account\Account;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
class CreateAccount extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'account:create
{--email= : Login email for the account.}
{--password= : Password to set for the account.}
{--firstname= : First name for the account.}
{--lastname= : Last name for the account.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new account';
/**
* Missing argument errors. Exposed for testing.
*/
const ERROR_MISSING_EMAIL = '! You must specify an email';
const ERROR_MISSING_PASSWORD = '! You must specify a password';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$email = $this->option('email');
if (empty($email)) {
$this->error($this::ERROR_MISSING_EMAIL);
}
$password = $this->option('password');
if (empty($password)) {
$this->error($this::ERROR_MISSING_PASSWORD);
}
$firstName = $this->option('firstname') ?? 'John';
$lastName = $this->option('lastname') ?? 'Doe';
if (empty($email) || empty($password)) {
return;
}
if ($this->confirmToProceed('This will create a new user for '.$firstName.' '.$lastName.' with email '.$email)) {
Account::createDefault($firstName, $lastName, $email, $password);
$this->info('| You can now sign in to your account:');
$this->line('| username: '.$email);
$this->line('| password: <hidden>');
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
use App\Jobs\SynchronizeAddressBooks;
use App\Models\Account\AddressBookSubscription;
class DavClientsUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:davclients';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update all dav subscriptions';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$subscriptions = AddressBookSubscription::active()->get();
$now = now();
$subscriptions->filter(function ($subscription) use ($now) {
return $this->isTimeToRunSync($subscription, $now);
})->each(function ($subscription) {
SynchronizeAddressBooks::dispatch($subscription);
});
}
/**
* Test if the last synchronized timestamp is older than the subscription's frequency time.
*
* @param AddressBookSubscription $subscription
* @param Carbon $now
* @return bool
*/
private function isTimeToRunSync(AddressBookSubscription $subscription, Carbon $now): bool
{
return is_null($subscription->last_synchronized_at)
|| $subscription->last_synchronized_at->addMinutes($subscription->frequency)->lessThan($now);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace App\Console\Commands;
use App\Models\User\User;
use Illuminate\Console\Command;
class Deactivate2FA extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = '2fa:deactivate
{--force : Force the operation to run when in production.}
{--email= : The email of the user to deactivate 2FA.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Deactivate 2FA for this user';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// retrieve the email from the option
$email = $this->option('email');
// if no email was passed to the option, prompt the user to enter the email
if (! $email) {
$email = $this->ask('what is the user\'s email?');
}
// retrieve the user with the specified email
$user = User::where('email', $email)->first();
if (! $user) {
// show an error and exist if the user does not exist
$this->error('No user with that email.');
return;
}
if (is_null($user->google2fa_secret)) {
// show an error and exist if the user does not exist
$this->error('2FA is currently not activated for this user.');
return;
}
// Print a warning
$this->info('2FA will be deactivated for '.$user->email);
$this->info('This action can\'t be cancelled.');
// ask for confirmation if not forced
if (! $this->option('force') && ! $this->confirm('Do you wish to continue?')) {
return;
}
// remove google2fa_secret key
$user->google2fa_secret = null;
// save the user
$user->save();
// show the new secret key
$this->info('2FA has been deactivated for '.$user->email);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Console\Commands;
use App\Jobs\ExportAllAsSQL;
use Illuminate\Console\Command;
class ExportAll extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'export:all';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Exports all data as SQL to storage/app';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$job = new ExportAllAsSQL();
$this->info('Exported as '.$job->handle().'');
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class GetVersion extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:getversion';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Get current version of monica';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$this->line(config('monica.app_version'));
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Console\Commands\Helpers;
use Tests\Helpers\CommandCallerFake;
/**
* @method static void exec(\Illuminate\Console\Command $command, string $message, string $commandline)
* @method static void artisan(\Illuminate\Console\Command $command, string $message, string $commandline, array $arguments)
*/
class Command
{
/**
* Switch to a fake executor for testing purpose.
*
* @return CommandCallerContract
*/
public static function fake(): CommandCallerContract
{
static::setBackend($fake = app(CommandCallerFake::class));
return $fake;
}
/**
* The Command Executor.
*
* @var CommandCallerContract|null
*/
private static $commandCaller;
/**
* Get the current backend command.
*
* @return CommandCallerContract
*/
private static function getBackend(): CommandCallerContract
{
if (! static::$commandCaller) {
static::$commandCaller = app(CommandCaller::class); // @codeCoverageIgnore
}
return static::$commandCaller;
}
/**
* Set the current backend command.
*
* @param CommandCallerContract $executor
*/
public static function setBackend(CommandCallerContract $executor): void
{
static::$commandCaller = $executor;
}
/**
* Handle dynamic, static calls to the object.
*
* @param string $method
* @param array $args
* @return mixed
*
* @throws \RuntimeException
*/
public static function __callStatic($method, $args)
{
$instance = static::getBackend();
return $instance->$method(...$args);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Console\Commands\Helpers;
use function Safe\exec;
use Illuminate\Console\Command;
use Illuminate\Console\Application;
use Symfony\Component\Console\Output\OutputInterface;
class CommandCaller implements CommandCallerContract
{
/**
* Print a message on the console, then execute a command.
*
* @param Command $command Laravel command context
* @param string $message Message to output
* @param string $commandline Command to execute
*
* @codeCoverageIgnore
*/
public function exec(Command $command, string $message, string $commandline): void
{
$command->info($message);
$command->line($commandline, null, OutputInterface::VERBOSITY_VERBOSE);
exec($commandline.' 2>&1', $output);
foreach ($output as $line) {
$command->line($line, null, OutputInterface::VERBOSITY_VERY_VERBOSE);
}
$command->line('', null, OutputInterface::VERBOSITY_VERBOSE);
}
/**
* Print a message on the console, then execute an artisan command.
*
* @param Command $command Laravel command context
* @param string $message Message to output
* @param string $commandline Artisan command name to execute
* @param array $arguments Optional arguments to pass to the artisan command
*
* @codeCoverageIgnore
*/
public function artisan(Command $command, string $message, string $commandline, array $arguments = []): void
{
$info = '';
foreach ($arguments as $key => $value) {
if (is_string($key)) {
$info .= ' '.$key.'="'.$value.'"';
} else {
$info .= ' '.$value;
}
}
$this->exec($command, $message, Application::formatCommandString($commandline.$info));
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Console\Commands\Helpers;
use Illuminate\Console\Command;
interface CommandCallerContract
{
/**
* Print a message on the console, then execute a command.
*
* @param Command $command Laravel command context
* @param string $message Message to output
* @param string $commandline Command to execute
*/
public function exec(Command $command, string $message, string $commandline): void;
/**
* Print a message on the console, then execute an artisan command.
*
* @param Command $command Laravel command context
* @param string $message Message to output
* @param string $commandline Artisan command name to execute
* @param array $arguments Optional arguments to pass to the artisan command
*/
public function artisan(Command $command, string $message, string $commandline, array $arguments = []): void;
}

View File

@@ -0,0 +1,134 @@
<?php
namespace App\Console\Commands;
use App\Models\Account\Account;
use Illuminate\Console\Command;
class ImportAccounts extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'account:import_ldap
{--ldap_uri= : LDAP URI.}
{--ldap_user= : LDAP Bind DN.}
{--ldap_pass= : LDAP Bind Password.}
{--ldap_base= : LDAP base DN for searching.}
{--ldap_filter= : Filter to search for user accounts.}
{--ldap_attr_mail= : LDAP attribute to map to email (default: mail).}
{--ldap_attr_firstname= : LDAP attribute to map to firstname (default: gn).}
{--ldap_attr_lastname= : LDAP attribute to map to lastname (default: sn).}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Import user accounts from LDAP';
/**
* Missing argument errors. Exposed for testing.
*/
const ERROR_MISSING_LDAP_FILTER = '! You must specify an LDAP Filter';
const ERROR_MISSING_LDAP_BASE = '! You must specify an LDAP Base';
const ERROR_MISSING_LDAP_USER = '! You must specify an LDAP User';
const ERROR_MISSING_LDAP_PASS = '! You must specify an LDAP Password';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$ldap_uri = $this->option('ldap_uri') ?? '127.0.0.1';
$ldap_attr_mail = $this->option('ldap_attr_mail') ?? 'mail';
$ldap_attr_firstname = $this->option('ldap_attr_firstname') ?? 'givenName';
$ldap_attr_lastname = $this->option('ldap_attr_lastname') ?? 'sn';
$ldap_user = $this->option('ldap_user');
if (empty($ldap_user)) {
$this->error($this::ERROR_MISSING_LDAP_USER);
}
$ldap_pass = $this->option('ldap_pass');
if (empty($ldap_pass)) {
$this->error($this::ERROR_MISSING_LDAP_PASS);
}
$ldap_base = $this->option('ldap_base');
if (empty($ldap_base)) {
$this->error($this::ERROR_MISSING_LDAP_BASE);
}
$ldap_filter = $this->option('ldap_filter');
if (empty($ldap_filter)) {
$this->error($this::ERROR_MISSING_LDAP_FILTER);
}
if (empty($ldap_user) || empty($ldap_pass) || empty($ldap_base) || empty($ldap_filter)) {
return;
}
$ldap_conn = ldap_connect($ldap_uri);
if (! $ldap_conn) {
$this->error('Could not connect to LDAP URI');
return;
}
if (! ldap_set_option($ldap_conn, LDAP_OPT_PROTOCOL_VERSION, 3)) {
$this->error('Could not set LDAP protocol v3');
return false;
}
try {
$bind = ldap_bind($ldap_conn, $ldap_user, $ldap_pass);
if (! $bind) {
$this->error('Could not bind with given LDAP credentials');
return;
}
} catch (\Exception $e) {
$this->error($e->getMessage());
return;
}
$ldap_res = [];
try {
$ldap_res = ldap_search($ldap_conn, $ldap_base, $ldap_filter, [$ldap_attr_mail, $ldap_attr_firstname, $ldap_attr_lastname]);
} catch (\Exception $e) {
$this->error($e->getMessage());
return;
}
$ldap_data = ldap_get_entries($ldap_conn, $ldap_res);
for ($i = 0; $i < $ldap_data['count']; $i++) {
if (! (isset($ldap_data[$i][$ldap_attr_mail]) && $ldap_data[$i][$ldap_attr_mail]['count'] > 0)) {
continue;
}
$user_mail = $ldap_data[$i][$ldap_attr_mail][0];
$user_firstname = 'John';
$user_lastname = 'Doe';
$user_password = bin2hex(random_bytes(64));
if (isset($ldap_data[$i][$ldap_attr_firstname]) && $ldap_data[$i][$ldap_attr_firstname]['count'] > 0) {
$user_firstname = $ldap_data[$i][$ldap_attr_firstname][0];
}
if (isset($ldap_data[$i][$ldap_attr_lastname]) && $ldap_data[$i][$ldap_attr_lastname]['count'] > 0) {
$user_lastname = $ldap_data[$i][$ldap_attr_lastname][0];
}
$this->info('Importing user "'.$user_mail.'"');
try {
Account::createDefault($user_firstname, $user_lastname, $user_mail, $user_password);
} catch (\Exception $import_error) {
$this->warn('Could not import user "'.$user_mail.'": '.$import_error->getMessage());
}
}
}
}

View File

@@ -0,0 +1,249 @@
<?php
namespace App\Console\Commands;
use function Safe\fopen;
use App\Models\User\User;
use function Safe\fclose;
use App\Helpers\DateHelper;
use App\Models\Contact\Gender;
use App\Models\Contact\Contact;
use Illuminate\Console\Command;
use App\Models\Contact\ContactField;
use App\Models\Contact\ContactFieldType;
use App\Jobs\Avatars\GetAvatarsFromInternet;
use App\Services\Contact\Address\CreateAddress;
use App\Services\Contact\Reminder\CreateReminder;
class ImportCSV extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'import:csv {user : email or id of a user} {file : path to the CSV file} {--format=google}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Imports CSV in Google format to user account';
/**
* The contact field email object.
*
* @var int|null
*/
public $contactFieldEmailId;
/**
* The contact field phone object.
*
* @var int|null
*/
public $contactFieldPhoneId;
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$file = $this->argument('file');
if (is_numeric($this->argument('user'))) {
$user = User::find($this->argument('user'));
} else {
$user = User::where('email', $this->argument('user'))->first();
}
if (! $user) {
$this->error('You need to provide a valid User ID or email address!');
return -1;
}
if (! file_exists($file)) {
$this->error('You need to provide a valid file path.');
return -2;
}
if (is_string($file)) {
$this->info("Importing CSV file {$file} to user {$user->id}");
}
// create special gender for this import
// we don't know which gender all the contacts are, so we need to create a special status for them, as we
// can't guess whether they are men, women or else.
$gender = Gender::where('name', config('dav.default_gender'))->first();
if (! $gender) {
$gender = new Gender;
$gender->account_id = $user->account_id;
$gender->name = config('dav.default_gender');
$gender->save();
}
$first = true;
$imported = 0;
$handle = fopen($file, 'r');
try {
while (($data = fgetcsv($handle)) !== false) { /** @phpstan-ignore-line */
// don't import the columns
if ($first) {
$first = false;
continue;
}
// if first & last name do not exist skip row
if (empty($data[1]) && empty($data[3])) {
continue;
}
$this->csvToContact($data, $user->account_id, $gender->id);
$imported++;
}
} finally {
fclose($handle);
}
$this->info("Imported {$imported} Contacts");
}
/**
* Create contact.
*/
private function csvToContact($data, $account_id, $gender_id)
{
$contact = new Contact();
$contact->account_id = $account_id;
$contact->gender_id = $gender_id;
if (! empty($data[1])) {
$contact->first_name = $data[1]; // Given Name
}
if (! empty($data[2])) {
$contact->middle_name = $data[2]; // Additional Name
}
if (! empty($data[3])) {
$contact->last_name = $data[3]; // Family Name
}
$street = null;
if (! empty($data[49])) {
$street = $data[49]; // address 1 street
}
$city = null;
if (! empty($data[50])) {
$city = $data[50]; // address 1 city
}
$province = null;
if (! empty($data[52])) {
$province = $data[52]; // address 1 region (state)
}
$postalCode = null;
if (! empty($data[53])) {
$postalCode = $data[53]; // address 1 postal code (zip) 53
}
if (! empty($data[66])) {
$contact->job = $data[66]; // organization 1 name 66
}
$contact->setAvatarColor();
$contact->save();
if (! empty($data[28])) {
// Email 1 Value
ContactField::firstOrCreate([
'account_id' => $contact->account_id,
'contact_id' => $contact->id,
'data' => $data[28],
'contact_field_type_id' => $this->contactFieldEmailId(),
]);
}
if ($postalCode || $province || $street || $city) {
$request = [
'account_id' => $contact->account_id,
'contact_id' => $contact->id,
'street' => $street,
'city' => $city,
'province' => $province,
'postal_code' => $postalCode,
];
app(CreateAddress::class)->execute($request);
}
if (! empty($data[42])) {
// Phone 1 Value
ContactField::firstOrCreate([
'account_id' => $contact->account_id,
'contact_id' => $contact->id,
'data' => $data[42],
'contact_field_type_id' => $this->contactFieldPhoneId(),
]);
}
if (! empty($data[14])) {
$birthdate = DateHelper::parseDate($data[14]);
$specialDate = $contact->setSpecialDate('birthdate', $birthdate->year, $birthdate->month, $birthdate->day);
app(CreateReminder::class)->execute([
'account_id' => $contact->account_id,
'contact_id' => $contact->id,
'initial_date' => DateHelper::getDate($specialDate),
'frequency_type' => 'year',
'frequency_number' => 1,
'title' => trans(
'people.people_add_birthday_reminder',
['name' => $contact->first_name]
),
'delible' => false,
]);
}
GetAvatarsFromInternet::dispatch($contact);
}
/**
* Get the default contact field email id for the account.
*
* @return int
*/
private function contactFieldEmailId()
{
if (! $this->contactFieldEmailId) {
$contactFieldType = ContactFieldType::where('type', 'email')->first();
$this->contactFieldEmailId = $contactFieldType->id;
}
return $this->contactFieldEmailId;
}
/**
* Get the default contact field phone id for the account.
*
* @return int
*/
private function contactFieldPhoneId()
{
if (! $this->contactFieldPhoneId) {
$contactFieldType = ContactFieldType::where('type', 'phone')->first();
$this->contactFieldPhoneId = $contactFieldType->id;
}
return $this->contactFieldPhoneId;
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace App\Console\Commands;
use App\Models\User\User;
use Illuminate\Http\File;
use Illuminate\Console\Command;
use App\Jobs\AddContactFromVCard;
use App\Models\Account\ImportJob;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;
class ImportVCards extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'import:vcard
{--user= : user to import the contacts}
{--path= : path of the file to import}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Imports contacts from vCard files for a specific user';
/**
* Execute the console command.
*
* @param Filesystem $filesystem
* @return mixed
*/
public function handle(Filesystem $filesystem)
{
$email = $this->option('user');
// if no email was passed to the option, prompt the user to enter the email
if (! $email) {
$email = $this->ask('what is the user\'s email?');
}
// retrieve the user with the specified email
$user = User::where('email', $email)->first();
if (! $user) {
// show an error and exist if the user does not exist
$this->error('No user with that email.');
return -1;
}
$path = $this->option('path');
// if no email was passed to the option, prompt the user to enter the email
if (! $path) {
$path = $this->ask('what file you want to import?');
}
if (! $filesystem->exists($path) || ! $this->acceptedExtensions($filesystem, $path)) {
$this->error('The provided vcard file was not found or is not valid!');
return -2;
}
$importJob = $this->import($path, $user);
return $this->report($importJob) ? 0 : 1;
}
private function acceptedExtensions(Filesystem $filesystem, string $path): bool
{
switch ($filesystem->extension($path)) {
case 'vcf':
case 'vcard':
return true;
default:
return false;
}
}
private function import(string $path, User $user): ImportJob
{
$pathName = Storage::putFile('public', new File($path));
$importJob = $user->account->importjobs()->create([
'user_id' => $user->id,
'type' => 'vcard',
'filename' => $pathName,
]);
AddContactFromVCard::dispatchSync($importJob);
return $importJob;
}
private function report(ImportJob $importJob)
{
$importJob->refresh();
if ($importJob->failed) {
$this->warn('Error: '.$importJob->failed_reason);
return false;
}
$this->info('Contacts found: '.$importJob->contacts_found);
$this->info('Contacts skipped: '.$importJob->contacts_skipped);
$this->info('Contacts imported: '.$importJob->contacts_imported);
return true;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Foundation\Inspiring;
class Inspire extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'inspire';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Display an inspiring quote';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$this->comment(PHP_EOL.Inspiring::quote().PHP_EOL);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Console\Commands;
use DirectoryIterator;
use Illuminate\Console\Command;
class LangGenerate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'lang:generate';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate i18n json assets';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$dirs = new DirectoryIterator(resource_path('lang').'/');
foreach ($dirs as $dir) {
if (! $dir->isDir()) {
continue;
}
$lang = $dir->getFilename();
if ($lang == '.' || $lang == '..') {
continue;
}
$this->call('lang:js', [
'--json' => true,
'--source' => $dir->getPathname(),
'target' => 'public/js/langs/'.$lang.'.json',
]);
}
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace App\Console\Commands;
use App\Helpers\DBHelper;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Console\ConfirmableTrait;
class MigrateDatabaseCollation extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'migrate:collation
{--force : Force the operation to run when in production.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update database for utf8mb4 collation';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if ($this->confirmToProceed()) {
try {
$connection = DBHelper::connection();
if ($connection->getDriverName() != 'mysql') {
return;
}
$databasename = $connection->getDatabaseName();
$schemata = $connection->table('information_schema.schemata')
->select('DEFAULT_CHARACTER_SET_NAME')
->where('schema_name', '=', $databasename)
->get();
$schema = $schemata->first()->DEFAULT_CHARACTER_SET_NAME;
if (config('database.use_utf8mb4') && $schema == 'utf8') {
$this->line('Migrate to utf8mb4 schema collation');
$this->toUtf8mb4($connection, $databasename);
} elseif (! config('database.use_utf8mb4') && $schema == 'utf8mb4') {
$this->line('Migrate to utf8 schema collation');
$this->toUtf8($connection, $databasename);
} else {
$this->info('Nothing to migrate, everything is ok.');
}
} catch (\Exception $e) {
$this->error(' ');
$this->error(' Check if the DB_USE_UTF8MB4 variable in .env file is correctly set ');
$this->error(' ');
$this->info('');
throw $e;
}
}
}
/**
* Switch to utf8mb4.
*
* @param \Illuminate\Database\Connection $connection
* @param string $databasename
*/
private function toUtf8mb4($connection, $databasename)
{
// Tables
$tables = $connection->table('information_schema.tables')
->select('table_name')
->where('table_schema', '=', $databasename)
->get();
foreach ($tables as $table) {
DB::statement('ALTER TABLE `'.$table->table_name.'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
}
// Database
$pdo = $connection->getPdo();
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, true);
DB::statement('ALTER DATABASE `'.$databasename.'` CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;');
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
}
/**
* Switch to utf8.
*
* @param \Illuminate\Database\Connection $connection
* @param string $databasename
*/
private function toUtf8($connection, $databasename)
{
// Tables
$tables = $connection->table('information_schema.tables')
->select('table_name')
->where('table_schema', '=', $databasename)
->get();
foreach ($tables as $table) {
DB::statement('ALTER TABLE `'.$table->table_name.'` CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;');
}
// Database
$pdo = $connection->getPdo();
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, true);
DB::statement('ALTER DATABASE `'.$databasename.'` CHARACTER SET = utf8 COLLATE = utf8_unicode_ci;');
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Console\Commands;
use App\Models\User\User;
use Illuminate\Console\Command;
use App\Jobs\SynchronizeAddressBooks;
use App\Services\DavClient\CreateAddressBookSubscription;
class NewAddressBookSubscription extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:newaddressbooksubscription
{--email= : Monica account to add subscription to}
{--url= : CardDAV url of the address book}
{--login= : Login}
{--password= : Password of the account}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Add a new all dav subscriptions';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$user = User::where('email', $this->option('email'))->firstOrFail();
$url = $this->option('url') ?? $this->ask('url', 'CardDAV url of the address book');
$login = $this->option('login') ?? $this->ask('login', 'Login name');
$password = $this->option('password') ?? $this->ask('password', 'User password');
try {
$addressBookSubscription = app(CreateAddressBookSubscription::class)->execute([
'account_id' => $user->account_id,
'user_id' => $user->id,
'base_uri' => $url,
'username' => $login,
'password' => $password,
]);
} catch (\Exception $e) {
$this->error($e->getMessage());
}
if (! isset($addressBookSubscription)) {
$this->error('Could not add subscription');
} else {
$this->info('Subscription added');
SynchronizeAddressBooks::dispatch($addressBookSubscription, true);
}
}
}

View File

@@ -0,0 +1,137 @@
<?php
namespace App\Console\Commands\OneTime;
use App\Models\Contact\Contact;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Illuminate\Console\ConfirmableTrait;
use Symfony\Component\Console\Output\OutputInterface;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
class MoveAvatars extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:moveavatars
{--force : Force the operation to run when in production.}
{--dryrun : Simulate the execution but not write anything.}
{--storage= : new storage to move avatars to}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Move avatars from their current storage to current default storage';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (! $this->confirmToProceed()) {
return;
}
Contact::where('has_avatar', true)
->chunk(200, function ($contacts) {
$this->handleContacts($contacts);
});
}
private function handleContacts($contacts)
{
foreach ($contacts as $contact) {
if ($contact->avatar_location == $this->newStorage()) {
continue;
}
try {
$this->handleOneContact($contact);
} catch (FileNotFoundException $e) {
continue;
}
}
}
private function handleOneContact($contact)
{
// move avatars to new location
$this->moveContactAvatars($contact);
if (! $this->option('dryrun')) {
$contact->deleteAvatars();
$this->line(' Files deleted from old location.', null, OutputInterface::VERBOSITY_VERBOSE);
// Update location. The filename has not changed.
$contact->avatar_location = $this->newStorage();
$contact->save();
}
}
private function moveContactAvatars($contact)
{
$this->line('Contact id:'.$contact->id.' | Avatar location:'.$contact->avatar_location.' | File name:'.$contact->avatar_file_name);
$avatarFileNames = [];
array_push($avatarFileNames, $this->getFileName($contact));
array_push($avatarFileNames, $this->getFileName($contact, 110));
array_push($avatarFileNames, $this->getFileName($contact, 174));
$storage = Storage::disk($contact->avatar_location);
$newStorage = Storage::disk($this->newStorage());
foreach ($avatarFileNames as $avatarFileName) {
if ($newStorage->exists($avatarFileName)) {
$this->line(' File already pushed: '.$avatarFileName, null, OutputInterface::VERBOSITY_VERBOSE);
continue;
}
if (! $this->option('dryrun')) {
$avatarFile = $storage->get($avatarFileName);
$newStorage->put($avatarFileName, $avatarFile, config('filesystems.default_visibility'));
}
$this->line(' File pushed: '.$avatarFileName, null, OutputInterface::VERBOSITY_VERBOSE);
}
}
private function getFileName($contact, $size = null)
{
$filename = pathinfo($contact->avatar_file_name, PATHINFO_FILENAME);
$extension = pathinfo($contact->avatar_file_name, PATHINFO_EXTENSION);
$avatarFileName = 'avatars/'.$filename.'.'.$extension;
if (! is_null($size)) {
$avatarFileName = 'avatars/'.$filename.'_'.$size.'.'.$extension;
}
if ($this->fileExists($contact->avatar_location, $avatarFileName)) {
return $avatarFileName;
}
}
private function fileExists($storage, $avatarFileName): bool
{
$storage = Storage::disk($storage);
if (! $storage->exists($avatarFileName)) {
$this->line(' ! File not found: '.$avatarFileName, null, OutputInterface::VERBOSITY_VERBOSE);
throw new FileNotFoundException();
}
return true;
}
private function newStorage()
{
return $this->option('storage') ?? config('filesystems.default');
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Console\Commands\OneTime;
use App\Events\MoveAvatarEvent;
use App\Models\Contact\Contact;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Event;
use Illuminate\Console\ConfirmableTrait;
use App\Exceptions\FileNotFoundException;
use Symfony\Component\Console\Output\OutputInterface;
use App\Jobs\Avatars\MoveContactAvatarToPhotosDirectory;
/**
* This command moves current avatars to the new Photos directory and converts
* each avatar to a Photo object.
*/
class MoveAvatarsToPhotosDirectory extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:moveavatarstophotosdirectory
{--force : Force the operation to run when in production.}
{--dryrun : Simulate the execution but not write anything.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Move avatars to the Photos directory, and create a photo object for each one of them';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (! $this->confirmToProceed()) {
return;
}
Event::listen(MoveAvatarEvent::class, function ($event) {
$this->handleEvent($event->contact);
});
$delay = now();
Contact::where('has_avatar', true)
->chunk(100, function ($contacts) use ($delay) {
foreach ($contacts as $contact) {
if ($contact->avatar_source === 'default') {
$this->handleContact($contact, $delay);
}
}
// add some delay, so we treat 100 contacts each minutes
$delay = $delay->addMinutes(1);
});
}
private function handleContact($contact, $delay)
{
try {
if ($this->option('dryrun')) {
MoveContactAvatarToPhotosDirectory::dispatchNow($contact, true);
} else {
MoveContactAvatarToPhotosDirectory::dispatch($contact, false)
->delay($delay);
}
} catch (FileNotFoundException $e) {
$this->warn(' ! File not found: '.$e->fileName, OutputInterface::VERBOSITY_VERBOSE);
}
}
private function handleEvent($contact)
{
$this->info('Contact id:'.$contact->id.' | Avatar location:'.$contact->avatar_location.' | File name:'.$contact->avatar_file_name);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Laravel\Passport\PersonalAccessClient;
use Symfony\Component\Console\Output\OutputInterface;
class Passport extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:passport {--force : Force the operation to run when in production.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check if encryption keys and Personal Access Client are present, and create them if not.';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
if ($this->confirmToProceed()) {
$this->checkEncryptionKeys();
$this->checkPersonalAccessClient();
}
}
private function checkEncryptionKeys()
{
$this->info('Checking encryption keys...', OutputInterface::VERBOSITY_VERBOSE);
if (! empty(config('passport.private_key')) && ! empty(config('passport.public_key'))) {
$this->info('✓ PASSPORT_PRIVATE_KEY and PASSPORT_PUBLIC_KEY detected.', OutputInterface::VERBOSITY_VERBOSE);
return;
}
if (file_exists(base_path('storage/oauth-private.key')) && file_exists(base_path('storage/oauth-public.key'))) {
$this->info('✓ Files storage/oauth-private.key and storage/oauth-public.key detected.', OutputInterface::VERBOSITY_VERBOSE);
return;
}
$this->artisan('✓ Creating encryption keys', 'passport:keys', ['--no-interaction']);
$this->warn('! Please be careful to backup '.base_path('storage/oauth-public.key').' and '.base_path('storage/oauth-private.key').' files !', OutputInterface::VERBOSITY_VERBOSE);
}
private function checkPersonalAccessClient()
{
$this->info('Checking Personal Access Client...', OutputInterface::VERBOSITY_VERBOSE);
if (PersonalAccessClient::count() > 0) {
$this->info('✓ Personal Access Client already created.', OutputInterface::VERBOSITY_VERBOSE);
return;
}
$this->artisan('✓ Creating personal access client', 'passport:client', ['--personal', '--no-interaction']);
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace App\Console\Commands;
use PharIo\Version\Version;
use App\Models\Contact\Contact;
use Illuminate\Console\Command;
use App\Models\Instance\Instance;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Http\Client\RequestException;
use Symfony\Component\Console\Output\OutputInterface;
class PingVersionServer extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:ping
{--force : Force the operation to run when in production.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Ping version.monicahq.com to know if a new version is available';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (! config('monica.check_version')) {
return;
}
if (! $this->confirmToProceed('Checking version deactivated', function () {
return $this->getLaravel()->environment() === 'production';
})) {
return false;
}
$instance = Instance::first();
$instance->current_version = config('monica.app_version');
if ($instance->current_version == '') {
Log::warning('Current instance version is not set, skipping version check.');
return;
}
// Query version.monicahq.com
try {
$this->log('Call url: '.config('monica.weekly_ping_server_url'));
$response = Http::acceptJson()
->post(config('monica.weekly_ping_server_url'), [
'uuid' => $instance->uuid,
'version' => $instance->current_version,
'contacts' => Contact::count(),
])
->throw();
} catch (RequestException $e) {
$this->error('Error calling "'.config('monica.weekly_ping_server_url').'": '.$e->getMessage());
Log::error(__CLASS__.' Error calling "'.config('monica.weekly_ping_server_url').'": '.$e->getMessage(), [$e]);
return;
}
// Receive the JSON
$json = $response->json();
$this->log('instance version: '.$instance->current_version);
$currentVersion = $this->getVersion($instance->current_version);
$this->log('current version: '.$json['latest_version']);
$latestVersion = $this->getVersion($json['latest_version']);
if ($latestVersion > $currentVersion) {
$instance->latest_version = $json['latest_version'];
$instance->latest_release_notes = $json['notes'];
$instance->number_of_versions_since_current_version = $json['number_of_versions_since_user_version'];
} else {
$instance->latest_release_notes = null;
$instance->number_of_versions_since_current_version = null;
}
$instance->save();
}
public function log($string)
{
$this->info($string, OutputInterface::VERBOSITY_VERBOSE);
}
private function getVersion(string $version): ?Version
{
try {
return new Version($version);
} catch (\Exception $e) {
$this->error("Error parsing version '$version': ".$e->getMessage());
}
return null;
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Contact\ReminderOutbox;
use App\Jobs\Reminder\NotifyUserAboutReminder;
class SendReminders extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'send:reminders';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send reminders that are scheduled for the contacts';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
// Grab all the reminders that are supposed to be sent in the next two days
// Why 2? because in terms of timezone, we can have up to more than 24 hours
// between two timezones and we need to take into accounts reminders
// that are not in the same timezone.
ReminderOutbox::where('planned_date', '<', now()->addDays(2))
->orderBy('planned_date', 'asc')
->chunk(500, function ($reminderOutboxes) {
$this->send($reminderOutboxes);
});
}
/**
* Send the reminder to the user and schedule the future.
*
* @return void
*/
private function send($reminderOutboxes)
{
foreach ($reminderOutboxes as $reminderOutbox) {
if ($reminderOutbox->user->isTheRightTimeToBeReminded($reminderOutbox->planned_date)) {
NotifyUserAboutReminder::dispatch($reminderOutbox);
}
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Console\Commands;
use App\Models\Contact\Contact;
use Illuminate\Console\Command;
use App\Jobs\StayInTouch\ScheduleStayInTouch;
class SendStayInTouch extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'send:stay_in_touch';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send notifications about staying in touch with contacts';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
// we add two days to make sure we cover all timezones
Contact::where('stay_in_touch_trigger_date', '<', now()->addDays(2))
->whereNotNull('stay_in_touch_frequency')
->orderBy('stay_in_touch_trigger_date', 'asc')
->chunk(500, function ($contacts) {
$this->schedule($contacts);
});
}
private function schedule($contacts)
{
foreach ($contacts as $contact) {
ScheduleStayInTouch::dispatch($contact);
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class SendTestEmail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:test-email
{--email= : The email address to send to}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send a test email to check that delivery is working';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
/** retrieve the email from the option.
* @var string $email
*/
$email = $this->option('email');
// if no email was passed to the option, prompt the user to enter the email
if (! $email) {
$email = (string) $this->ask('What email address should I send the test email to?');
}
// Validate user provided email address
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
$this->error("Invalid email address: \"$email\".");
return -1;
}
$this->info("Preparing and sending email to \"$email\"");
// immediately deliver the test email (bypassing the queue)
Mail::raw(
"Hi $email, you requested a test email from Monica.",
function ($message) use ($email) {
$message->to($email)
->subject('Monica email delivery test');
}
);
$this->info('Email sent!');
return 0;
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace App\Console\Commands;
use function Safe\exec;
use function Safe\mkdir;
use Illuminate\Console\Command;
use function Safe\file_put_contents;
use Illuminate\Console\ConfirmableTrait;
use Symfony\Component\Console\Output\OutputInterface;
class SentryRelease extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'sentry:release
{--force : Force the operation to run when in production.}
{--release= : release version for sentry.}
{--store-release : store release version in config/.release file.}
{--commit= : commit associated with this release.}
{--environment= : sentry environment.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a release for sentry';
/**
* Installation path of sentry cli.
*
* @var string
*/
private $install_dir;
/**
* sentry cli name.
*
* @var string
*/
private const SENTRY_CLI = 'sentry-cli';
/**
* Sentry cli download url.
*
* @var string
*/
private const SENTRY_URL = 'https://sentry.io/get-cli/';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (! config('monica.sentry_support') || ! $this->check()) {
return;
}
if ($this->confirmToProceed()) {
$this->install_dir = env('SENTRY_ROOT', getenv('HOME').'/.local/bin');
$release = $this->option('release') ?? config('sentry.release');
$commit = $this->option('commit') ??
(is_dir(__DIR__.'/../../../.git') ? trim(exec('git log --pretty="%H" -n1 HEAD')) : $release);
// Sentry update
$this->exec('Update sentry', $this->getSentryCli().' update');
// Create a release
$this->execSentryCli('Create a release', 'releases new '.$release.' --finalize --project '.config('sentry-release.project'));
// Associate commits with the release
$this->execSentryCli('Associate commits with the release', 'releases set-commits '.$release.' --commit "'.config('sentry-release.repo').'@'.$commit.'"');
// Create a deploy
$this->execSentryCli('Create a deploy', 'releases deploys '.$release.' new --env '.$this->option('environment').' --name '.config('monica.app_version'));
if ($this->option('store-release')) {
// Set sentry release
$this->line('Store release in config/.release file', null, OutputInterface::VERBOSITY_VERBOSE);
file_put_contents(__DIR__.'/../../../config/.release', $release);
}
}
}
private function check(): bool
{
$check = true;
if (empty(config('sentry-release.auth_token'))) {
$this->error('You must provide an auth_token (SENTRY_AUTH_TOKEN)');
$check = false;
}
if (empty(config('sentry-release.organisation'))) {
$this->error('You must provide an organisation slug (SENTRY_ORG)');
$check = false;
}
if (empty(config('sentry-release.project'))) {
$this->error('You must set the project (SENTRY_PROJECT)');
$check = false;
}
if (empty(config('sentry-release.repo'))) {
$this->error('You must set the repository (SENTRY_REPO)');
$check = false;
}
if (empty($this->option('environment'))) {
$this->error('No environment given');
$check = false;
}
return $check;
}
private function getSentryCli()
{
if (! file_exists($this->install_dir.'/'.self::SENTRY_CLI)) {
mkdir($this->install_dir, 0777, true);
$this->exec('Downloading sentry-cli', 'curl -sL '.self::SENTRY_URL.' | INSTALL_DIR='.$this->install_dir.' bash');
}
return $this->install_dir.'/'.self::SENTRY_CLI;
}
private function execSentryCli($message, $command)
{
$this->exec($message, $this->getSentryCli().' '.$command);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Console\Commands;
use App\Models\Account\Account;
use Illuminate\Console\Command;
class SetPremiumAccount extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'account:setpremium {accountId}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Give a premium access to an account.';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$account = Account::findOrFail($this->argument('accountId'));
$account->update([
'has_access_to_paid_version_for_free' => true,
]);
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Console\Commands;
use App\Models\User\User;
use Illuminate\Console\Command;
class SetUserAdmin extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:admin
{--force : Force the operation to run when in production.}
{--email= : The email of the user whose admin status you want to change}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Toggle administrator privileges for a user';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// retrieve the email from the option
$email = $this->option('email');
// if no email was passed to the option, prompt the user to enter the email
if (! $email) {
$email = $this->ask('What is the users email?');
}
// retrieve the user with the specified email
$user = User::where('email', $email)->first();
if (! $user) {
// show an error and exist if the user does not exist
$this->error('No user with that email.');
return;
}
// Print a warning
if ($user->admin) {
$this->warn($user->email.' will be removed from the administrators of this instance');
} else {
$this->warn($user->email.' will be added to the administrators of this instance');
}
// ask for confirmation if not forced
if (! $this->option('force') && ! $this->confirm('Do you wish to continue?')) {
return;
}
// toglle admin status
$user->admin = ! $user->admin;
$user->save();
// Show new status
if ($user->admin) {
$this->info($user->email.' has been added to the administrators of this instance');
} else {
$this->info($user->email.' has been removed from the administrators of this instance');
}
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Console\Commands;
use function Safe\touch;
use App\Helpers\InstanceHelper;
use App\Models\Account\Account;
use Illuminate\Console\Command;
class SetupProduction extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'setup:production
{--force : Force the operation to run when in production.}
{--email= : Login email for the first account.}
{--password= : Password to set for the first account.}
{--skipSeed : Skip the populate database process.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform setup of Monica.';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if ((! $this->option('force')) && (! $this->confirm('You are about to setup and configure Monica. Do you wish to continue?'))) {
return;
}
/*
* If the .env file does not exist, then key generation
* will fail. So we create one if it does not already exist.
*/
if (! file_exists(__DIR__.'/../../../.env')) {
touch(__DIR__.'/../../../.env');
}
$this->call('monica:update', ['--force' => true]);
if (! $this->option('skipSeed')) {
$this->line('✓ Filling database');
$this->call('db:seed', ['--force' => true]);
}
$this->line('');
$this->line('-----------------------------');
$this->line('|');
$this->line('| Welcome to Monica v'.config('monica.app_version'));
$this->line('|');
$this->line('-----------------------------');
$email = $this->option('email');
$password = $this->option('password');
if (! empty($email) && ! empty($password)) {
Account::createDefault('John', 'Doe', $email, $password);
$this->info('| You can now sign in to your account:');
$this->line('| username: '.$email);
$this->line('| password: <hidden>');
} elseif (InstanceHelper::hasAtLeastOneAccount()) {
$this->info('| You can now log in to your account');
} else {
$this->info('| You can now register to the first account by opening the application:');
}
$this->line('| URL: '.config('app.url'));
$this->line('-----------------------------');
$this->info('Setup is done. Have fun.');
}
}

View File

@@ -0,0 +1,677 @@
<?php
namespace App\Console\Commands;
use function Safe\exec;
use App\Models\User\User;
use App\Helpers\DateHelper;
use Illuminate\Support\Carbon;
use App\Models\Account\Account;
use App\Models\Contact\Contact;
use Illuminate\Console\Command;
use App\Helpers\CountriesHelper;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Console\Application;
use App\Models\Contact\LifeEventType;
use App\Models\Contact\ContactFieldType;
use App\Services\Contact\Gift\CreateGift;
use App\Services\Contact\Tag\AssociateTag;
use Illuminate\Foundation\Testing\WithFaker;
use App\Services\Contact\Address\CreateAddress;
use App\Services\Contact\Contact\CreateContact;
use App\Services\Contact\Reminder\CreateReminder;
use Symfony\Component\Console\Helper\ProgressBar;
use App\Services\Contact\LifeEvent\CreateLifeEvent;
use Symfony\Component\Console\Output\ConsoleOutput;
use App\Services\Contact\Conversation\CreateConversation;
use App\Services\Contact\Relationship\CreateRelationship;
use App\Services\Account\Activity\Activity\CreateActivity;
use App\Services\Contact\Contact\UpdateBirthdayInformation;
use App\Services\Contact\Contact\UpdateDeceasedInformation;
use App\Services\Contact\Conversation\AddMessageToConversation;
use App\Services\Account\Activity\Activity\AttachContactToActivity;
class SetupTest extends Command
{
use WithFaker;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'setup:test
{--skipSeed : Skip the populate database with fake data.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create the test environment with optional fake data for testing purposes.';
/**
* The number of contacts that need to be generated.
*
* @var int
*/
private $numberOfContacts;
/**
* The Current contact instance.
*
* @var Contact
*/
private $contact;
/**
* The current Contact instance.
*
* @var Account
*/
private $account;
/**
* The list of countries.
*
* @var Collection
*/
private $countries = null;
/**
* The logged user.
*
* @var User
*/
private $user;
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (! $this->confirm('Are you sure you want to proceed? This will delete ALL data in your environment.')) {
return;
}
$this->artisan('✓ Performing migrations', 'migrate:fresh');
$this->artisan('✓ Symlink the storage folder', 'storage:link');
if (! $this->option('skipSeed')) {
$this->numberOfContacts = $this->ask('How many contacts would you like to have in this test account?');
$this->info('✓ Filling database with fake data');
$this->seed();
}
$this->line('');
$this->line('-----------------------------');
$this->line('|');
$this->line('| Welcome to Monica v'.config('monica.app_version'));
$this->line('|');
$this->line('-----------------------------');
$this->info('| You can now sign in to your account:');
$this->line('| username: admin@admin.com');
$this->line('| password: admin0');
$this->line('| URL: '.config('app.url'));
$this->line('-----------------------------');
$this->info('Setup is done. Have fun.');
}
public function exec($message, $command)
{
$this->info($message);
$this->line($command);
exec($command, $output);
$this->line(implode('\n', $output));
$this->line('');
}
public function artisan($message, $command, array $arguments = [])
{
$this->info($message);
$this->line(Application::formatCommandString($command));
$this->callSilent($command, $arguments);
$this->line('');
}
/**
* Run the database seeds.
*
* @return void
*/
public function seed()
{
$this->setUpFaker();
// Get or create the first account
if (User::where('email', 'admin@admin.com')->exists()) {
$this->user = User::where('email', 'admin@admin.com')->first();
$userId = $this->user->value('id');
$this->account = Account::where('id', $userId)->first();
} else {
$this->account = Account::createDefault('John', 'Doe', 'admin@admin.com', 'admin0');
// set default admin account to confirmed
/** @var User */
$adminUser = $this->account->users()->first();
$this->confirmUser($adminUser);
$this->user = $adminUser;
}
// create a random number of contacts
//$this->numberOfContacts = rand(60, 100);
echo 'Generating '.$this->numberOfContacts.' fake contacts'.PHP_EOL;
$output = new ConsoleOutput();
$progress = new ProgressBar($output, $this->numberOfContacts);
$progress->start();
for ($i = 0; $i < $this->numberOfContacts; $i++) {
$gender = (rand(1, 2) == 1) ? 'male' : 'female';
$this->contact = app(CreateContact::class)->execute([
'account_id' => $this->account->id,
'author_id' => $this->user->id,
'first_name' => $this->faker->firstName($gender),
'last_name' => (rand(1, 2) == 1) ? $this->faker->lastName : null,
'nickname' => (rand(1, 2) == 1) ? $this->faker->name : null,
'gender_id' => $this->getRandomGender()->id,
'is_partial' => false,
'is_birthdate_known' => false,
'is_deceased' => false,
'is_deceased_date_known' => false,
]);
$this->populateTags();
$this->populateFoodPreferences();
$this->populateDeceasedDate();
$this->populateBirthday();
$this->populateFirstMetInformation();
$this->populateRelationships();
$this->populateNotes();
$this->populateActivities();
$this->populateTasks();
$this->populateDebts();
$this->populateCalls();
$this->populateConversations();
$this->populateLifeEvents();
$this->populateGifts();
$this->populateAddresses();
$this->populateContactFields();
$this->populatePets();
$this->changeUpdatedAt();
$progress->advance();
}
$this->populateDayRatings();
$this->populateEntries();
$progress->finish();
// create the second test, blank account
if (! User::where('email', 'blank@blank.com')->exists()) {
$blankAccount = Account::createDefault('Blank', 'State', 'blank@blank.com', 'blank0');
$blankUser = $blankAccount->users()->first();
$this->confirmUser($blankUser);
}
}
public function populateTags()
{
if (rand(1, 2) == 1) {
$i = 0;
do {
app(AssociateTag::class)->execute([
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'name' => $this->faker->word,
]);
$i++;
} while ($i < 10);
}
}
public function populateFoodPreferences()
{
// add food preferences
if (rand(1, 2) == 1) {
$this->contact->food_preferences = $this->faker->realText();
$this->contact->save();
}
}
public function populateDeceasedDate()
{
// deceased?
if (rand(1, 7) == 1) {
$birthdate = $this->faker->dateTimeThisCentury();
app(UpdateDeceasedInformation::class)->execute([
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'is_deceased' => rand(1, 2) == 1,
'is_date_known' => rand(1, 2) == 1,
'day' => (int) $birthdate->format('d'),
'month' => (int) $birthdate->format('m'),
'year' => (int) $birthdate->format('Y'),
'add_reminder' => rand(1, 2) == 1,
]);
}
}
public function populateBirthday()
{
if (rand(1, 2) == 1) {
$birthdate = $this->faker->dateTimeThisCentury();
app(UpdateBirthdayInformation::class)->execute([
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'is_date_known' => rand(1, 2) == 1,
'day' => (int) $birthdate->format('d'),
'month' => (int) $birthdate->format('m'),
'year' => (int) $birthdate->format('Y'),
'is_age_based' => rand(1, 2) == 1,
'age' => rand(1, 99),
'add_reminder' => rand(1, 2) == 1,
'is_deceased' => $this->contact->is_dead,
]);
}
}
public function populateFirstMetInformation()
{
if (rand(1, 2) == 1) {
$this->contact->first_met_where = $this->faker->realText(20);
}
if (rand(1, 2) == 1) {
$this->contact->first_met_additional_info = $this->faker->realText(20);
$firstMetDate = $this->faker->dateTimeThisCentury();
if (rand(1, 2) == 1) {
// add a date where we don't know the year
$specialDate = $this->contact->setSpecialDate('first_met', 0, intval($firstMetDate->format('m')), intval($firstMetDate->format('d')));
} else {
// add a date where we know the year
$specialDate = $this->contact->setSpecialDate('first_met', intval($firstMetDate->format('Y')), intval($firstMetDate->format('m')), intval($firstMetDate->format('d')));
}
app(CreateReminder::class)->execute([
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'initial_date' => $specialDate->date->toDateString(),
'frequency_type' => 'year',
'frequency_number' => 1,
'title' => trans(
'people.introductions_reminder_title',
['name' => $this->contact->first_name]
),
]);
}
if (rand(1, 2) == 1) {
do {
$rand = rand(1, $this->numberOfContacts);
} while (in_array($rand, [$this->contact->id]));
$this->contact->first_met_through_contact_id = $rand;
}
$this->contact->save();
}
public function populateRelationships()
{
if (rand(1, 2) == 1) {
foreach (range(1, rand(2, 6)) as $index) {
$gender = (rand(1, 2) == 1) ? 'male' : 'female';
$relatedContact = app(CreateContact::class)->execute([
'account_id' => $this->account->id,
'author_id' => $this->user->id,
'first_name' => $this->faker->firstName($gender),
'last_name' => (rand(1, 2) == 1) ? $this->faker->lastName : null,
'nickname' => (rand(1, 2) == 1) ? $this->faker->name : null,
'gender_id' => $this->getRandomGender()->id,
'is_partial' => rand(1, 2) == 1,
'is_birthdate_known' => false,
'is_deceased' => false,
'is_deceased_date_known' => false,
]);
$relatedContactBirthDate = $this->faker->dateTimeThisCentury();
app(UpdateBirthdayInformation::class)->execute([
'account_id' => $this->account->id,
'contact_id' => $relatedContact->id,
'is_date_known' => rand(1, 2) == 1,
'day' => (int) $relatedContactBirthDate->format('d'),
'month' => (int) $relatedContactBirthDate->format('m'),
'year' => (int) $relatedContactBirthDate->format('Y'),
'is_age_based' => rand(1, 2) == 1,
'age' => rand(1, 99),
'add_reminder' => rand(1, 2) == 1,
'is_deceased' => $relatedContact->is_dead,
]);
// set relationship
$relationshipId = $this->contact->account->relationshipTypes->random()->id;
$relationship = app(CreateRelationship::class)->execute([
'account_id' => $this->account->id,
'contact_is' => $this->contact->id,
'of_contact' => $relatedContact->id,
'relationship_type_id' => $relationshipId,
]);
}
}
}
public function populateNotes()
{
if (rand(1, 2) == 1) {
for ($j = 0; $j < rand(1, 13); $j++) {
$note = $this->contact->notes()->create([
'body' => $this->faker->realText(rand(40, 500)),
'account_id' => $this->account->id,
'is_favorited' => rand(1, 3) == 1,
'favorited_at' => $this->faker->dateTimeThisCentury(),
]);
}
}
}
public function populateActivities()
{
if (rand(1, 2) == 1) {
for ($j = 0; $j < rand(1, 13); $j++) {
$date = DateHelper::getDate(Carbon::instance($this->faker->dateTimeThisYear($max = 'now')));
$request = [
'account_id' => $this->account->id,
'activity_type_id' => rand(1, 13),
'summary' => $this->faker->realText(rand(40, 100)),
'description' => (rand(1, 2) == 1 ? $this->faker->realText(rand(100, 1000)) : null),
'happened_at' => $date,
'contacts' => [$this->contact->id],
];
$activity = app(CreateActivity::class)->execute($request);
$request = [
'account_id' => $this->account->id,
'activity_id' => $activity->id,
'contacts' => [$this->contact->id],
];
app(AttachContactToActivity::class)->execute($request);
DB::table('journal_entries')->insertGetId([
'account_id' => $this->account->id,
'date' => $date,
'journalable_id' => $activity->id,
'journalable_type' => 'App\Models\Account\Activity',
]);
}
}
}
public function populateTasks()
{
if (rand(1, 2) == 1) {
for ($j = 0; $j < rand(1, 10); $j++) {
$task = $this->contact->tasks()->create([
'title' => $this->faker->realText(rand(40, 100)),
'description' => $this->faker->realText(rand(100, 1000)),
'completed' => (rand(1, 2) == 1 ? 0 : 1),
'completed_at' => (rand(1, 2) == 1 ? $this->faker->dateTimeThisCentury() : null),
'account_id' => $this->account->id,
]);
}
}
}
public function populateDebts()
{
if (rand(1, 2) == 1) {
for ($j = 0; $j < rand(1, 6); $j++) {
$this->contact->debts()->create([
'in_debt' => (rand(1, 2) == 1 ? 'yes' : 'no'),
'amount' => rand(321, 39391),
'reason' => $this->faker->realText(rand(100, 1000)),
'status' => 'inprogress',
'account_id' => $this->account->id,
]);
}
}
}
public function populateGifts()
{
if (rand(1, 2) == 1) {
for ($j = 0; $j < rand(1, 31); $j++) {
app(CreateGift::class)->execute([
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'status' => (rand(1, 3) == 1 ? 'offered' : 'idea'),
'name' => $this->faker->realText(rand(10, 100)),
'comment' => $this->faker->realText(rand(1000, 5000)),
'url' => $this->faker->url,
'amount' => rand(12, 120),
]);
}
}
}
public function populateAddresses()
{
if (rand(1, 3) == 1) {
$request = [
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'country' => $this->getRandomCountry(),
'name' => $this->faker->word,
'street' => (rand(1, 3) == 1) ? $this->faker->streetAddress : null,
'city' => (rand(1, 3) == 1) ? $this->faker->city : null,
'province' => (rand(1, 3) == 1) ? $this->faker->state : null,
'postal_code' => (rand(1, 3) == 1) ? $this->faker->postcode : null,
];
app(CreateAddress::class)->execute($request);
}
}
private function getRandomCountry()
{
if ($this->countries == null) {
$this->countries = CountriesHelper::getAll();
}
return $this->countries->random()['id'];
}
public function populateContactFields()
{
if (rand(1, 3) == 1) {
// Fetch number of types
$numberOfTypes = ContactFieldType::where('account_id', $this->account->id)->count();
for ($j = 0; $j < rand(1, $numberOfTypes); $j++) {
// Retrieve random ContactFieldType
$contactFieldType = ContactFieldType::where('account_id', $this->account->id)->orderBy(DB::raw('RAND()'))->firstOrFail();
// Fake data according to type
$data = null;
switch ($contactFieldType->name) {
case 'Email':
$data = $this->faker->email;
break;
case 'Phone':
$data = $this->faker->phoneNumber;
break;
case 'Facebook':
$data = 'https://facebook.com/'.$this->faker->userName;
break;
case 'Twitter':
$data = 'https://twitter.com/'.$this->faker->userName;
break;
case 'Whatsapp':
$data = $this->faker->phoneNumber;
break;
case 'Telegram':
$data = $this->faker->phoneNumber;
break;
default:
$data = $this->faker->url;
break;
}
$this->contact->contactFields()->create([
'contact_field_type_id' => $contactFieldType->id,
'data' => $data,
'account_id' => $this->account->id,
]);
}
}
}
public function populateEntries()
{
for ($j = 0; $j < rand(10, 100); $j++) {
$date = $this->faker->dateTimeThisYear();
$entryId = DB::table('entries')->insertGetId([
'account_id' => $this->account->id,
'title' => $this->faker->realText(rand(12, 20)),
'post' => $this->faker->realText(rand(400, 500)),
'created_at' => $date,
]);
DB::table('journal_entries')->insertGetId([
'account_id' => $this->account->id,
'date' => $date,
'journalable_id' => $entryId,
'journalable_type' => 'App\Models\Journal\Entry',
'created_at' => now(),
]);
}
}
public function populatePets()
{
if (rand(1, 3) == 1) {
for ($j = 0; $j < rand(1, 3); $j++) {
$date = $this->faker->dateTimeThisYear();
DB::table('pets')->insertGetId([
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'pet_category_id' => rand(1, 11),
'name' => (rand(1, 3) == 1) ? $this->faker->firstName : null,
'created_at' => $date,
]);
}
}
}
public function populateDayRatings()
{
for ($j = 0; $j < rand(10, 100); $j++) {
$date = $this->faker->dateTimeThisYear();
$dayId = DB::table('days')->insertGetId([
'account_id' => $this->account->id,
'rate' => rand(1, 3),
'date' => $date,
'created_at' => $date,
]);
DB::table('journal_entries')->insertGetId([
'account_id' => $this->account->id,
'date' => $date,
'journalable_id' => $dayId,
'journalable_type' => 'App\Models\Journal\Day',
'created_at' => now(),
]);
}
}
public function changeUpdatedAt()
{
$this->contact->last_consulted_at = Carbon::instance($this->faker->dateTimeThisYear());
$this->contact->save();
}
public function populateCalls()
{
if (rand(1, 3) == 1) {
$this->contact->calls()->create([
'account_id' => $this->account->id,
'called_at' => $this->faker->dateTimeThisYear(),
]);
}
}
public function populateConversations()
{
if (rand(1, 3) == 1) {
for ($j = 0; $j < rand(1, 20); $j++) {
$contactFieldType = ContactFieldType::where('account_id', $this->account->id)->orderBy(DB::raw('RAND()'))->firstOrFail();
$conversation = app(CreateConversation::class)->execute([
'happened_at' => $this->faker->dateTimeThisCentury(),
'contact_id' => $this->contact->id,
'contact_field_type_id' => $contactFieldType->id,
'account_id' => $this->account->id,
]);
for ($k = 0; $k < rand(1, 20); $k++) {
app(AddMessageToConversation::class)->execute([
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'conversation_id' => $conversation->id,
'written_at' => $this->faker->dateTimeThisCentury(),
'written_by_me' => (rand(1, 2) == 1),
'content' => $this->faker->realText(),
]);
}
}
}
}
public function populateLifeEvents()
{
if (rand(1, 3) == 1) {
for ($j = 0; $j < rand(1, 20); $j++) {
$lifeEventType = LifeEventType::where('account_id', $this->account->id)->orderBy(DB::raw('RAND()'))->firstOrFail();
app(CreateLifeEvent::class)->execute([
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'life_event_type_id' => $lifeEventType->id,
'happened_at' => $this->faker->dateTimeThisCentury(),
'name' => $this->faker->realText(),
'note' => $this->faker->realText(),
'has_reminder' => false,
'happened_at_month_unknown' => false,
'happened_at_day_unknown' => false,
]);
}
}
}
public function getRandomGender()
{
return $this->account->genders->random();
}
public function confirmUser($user)
{
$user->markEmailAsVerified();
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Console\Commands\Tests;
use App\Models\User\User;
use Illuminate\Console\Command;
use App\Services\User\AcceptPolicy;
class SetupFrontEndTestUser extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'setup:frontendtestuser {--database= : The database connection to use}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a user exclusively for front-end testing with Cypress.';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
app('migrator')->setConnection($this->option('database'));
$user = factory(User::class)->create();
$user->account->populateDefaultFields();
app(AcceptPolicy::class)->execute([
'account_id' => $user->account->id,
'user_id' => $user->id,
'ip_address' => null,
]);
$this->info($user->getKey());
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Console\Commands\Tests;
use Illuminate\Support\Str;
use function Safe\json_decode;
use function Safe\json_encode;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
/**
* @codeCoverageIgnore
*/
class UpdateLangs extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'setup:updatelang';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update langage files.';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
if (! File::exists(base_path('vendor/laravel-lang/lang'))) {
throw new \Exception("This command requires laravel-lang/lang package. Run 'composer require --dev laravel-lang/lang' to install it.");
}
$en = json_decode(File::get(lang_path('en.json')), true);
foreach (File::directories(lang_path()) as $lang) {
$lang = basename($lang);
$locale = Str::of($lang)->replace('-', '_');
switch ($lang) {
case 'zh':
$locale = 'zh_CN';
break;
case 'en':
continue 2;
}
if (File::exists($orig_path = lang_path("$lang.json"))
&& File::exists($trans_path = base_path("vendor/laravel-lang/lang/locales/$locale/$locale.json"))) {
$lang_orig = json_decode(File::get($orig_path), true);
$lang_trans = json_decode(File::get($trans_path), true);
foreach ($en as $key) {
$lang_orig[$key] = $lang_trans[$key];
}
File::put($orig_path, json_encode($lang_orig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)."\n");
}
}
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace App\Console\Commands;
use App\Helpers\DBHelper;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Console\ConfirmableTrait;
class Update extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:update
{--force : Force the operation to run when in production.}
{--composer-install : Updating composer dependencies.}
{--skip-storage-link : Skip storage link create.}
{--dev : Install dev dependencies too.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update monica dependencies and migrations after a new release';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
if ($this->confirmToProceed()) {
try {
$this->artisan('✓ Maintenance mode: on', 'down', [
'--retry' => '10',
]);
// Clear or rebuild all cache
if (config('cache.default') != 'database' || Schema::hasTable(config('cache.stores.database.table'))) {
$this->artisan('✓ Resetting application cache', 'cache:clear');
}
if ($this->getLaravel()->environment() == 'production') {
$this->artisan('✓ Clear config cache', 'config:clear');
$this->artisan('✓ Resetting route cache', 'route:cache');
if ($this->getLaravel()->version() > '5.6') {
$this->artisan('✓ Resetting view cache', 'view:cache');
} else {
$this->artisan('✓ Resetting view cache', 'view:clear');
}
} else {
$this->artisan('✓ Clear config cache', 'config:clear');
$this->artisan('✓ Clear route cache', 'route:clear');
$this->artisan('✓ Clear view cache', 'view:clear');
}
if ($this->option('composer-install') === true) {
$this->exec('✓ Updating composer dependencies', 'composer install --no-interaction'.($this->option('dev') === false ? ' --no-dev' : ''));
}
if ($this->option('skip-storage-link') !== true && $this->getLaravel()->environment() != 'testing' && ! file_exists(public_path('storage'))) {
$this->artisan('✓ Symlink the storage folder', 'storage:link');
}
if ($this->migrateCollationTest()) {
$this->artisan('✓ Performing collation migrations', 'migrate:collation', ['--force']);
}
$this->artisan('✓ Performing migrations', 'migrate', ['--force']);
$this->artisan('✓ Check for encryption keys', 'monica:passport', ['--force']);
$this->artisan('✓ Ping for new version', 'monica:ping', ['--force']);
// Cache config
if ($this->getLaravel()->environment() == 'production'
&& (config('cache.default') != 'database' || Schema::hasTable(config('cache.stores.database.table')))) {
$this->artisan('✓ Cache configuraton', 'config:cache');
}
} finally {
$this->artisan('✓ Maintenance mode: off', 'up');
}
$this->line('Monica v'.config('monica.app_version').' is set up, enjoy.');
}
}
private function migrateCollationTest()
{
$connection = DBHelper::connection();
if ($connection->getDriverName() != 'mysql') {
return false;
}
$databasename = $connection->getDatabaseName();
$schemata = DB::select(
'select DEFAULT_CHARACTER_SET_NAME from information_schema.schemata where schema_name = ?',
[$databasename]
);
$schema = $schemata[0]->DEFAULT_CHARACTER_SET_NAME;
return config('database.use_utf8mb4') && $schema == 'utf8'
|| ! config('database.use_utf8mb4') && $schema == 'utf8mb4';
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Jobs\Avatars\UpdateAllGravatars;
class UpdateGravatars extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:updategravatars';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update all gravatars';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
UpdateAllGravatars::dispatch();
}
}