diff --git a/.env b/.env index c7c3476b06..aebffe00a9 100644 --- a/.env +++ b/.env @@ -47,8 +47,9 @@ _APP_SMTP_PORT=1025 _APP_SMTP_SECURE= _APP_SMTP_USERNAME= _APP_SMTP_PASSWORD= -_APP_HAMSTER_RECIPIENTS= _APP_HAMSTER_INTERVAL=86400 +_APP_HAMSTER_TIME=21:00 +_APP_MIXPANEL_TOKEN= _APP_SMS_PROVIDER=sms://username:password@mock _APP_SMS_FROM=+123456789 _APP_STORAGE_LIMIT=30000000 @@ -77,4 +78,6 @@ _APP_LOGGING_PROVIDER= _APP_LOGGING_CONFIG= _APP_REGION=default _APP_DOCKER_HUB_USERNAME= -_APP_DOCKER_HUB_PASSWORD= \ No newline at end of file +_APP_DOCKER_HUB_PASSWORD= +_APP_CONSOLE_GITHUB_SECRET= +_APP_CONSOLE_GITHUB_APP_ID= \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index b87d6e912c..84e5d1dc56 100755 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ ENV VITE_APPWRITE_GROWTH_ENDPOINT=$VITE_APPWRITE_GROWTH_ENDPOINT RUN npm ci RUN npm run build -FROM appwrite/base:0.1.0 as final +FROM appwrite/base:0.2.2 as final LABEL maintainer="team@appwrite.io" @@ -136,6 +136,7 @@ COPY --from=node /usr/local/src/console/build /usr/src/code/console # Add Source Code COPY ./app /usr/src/code/app +COPY ./public /usr/src/code/public COPY ./bin /usr/local/bin COPY ./docs /usr/src/code/docs COPY ./src /usr/src/code/src @@ -156,6 +157,8 @@ RUN mkdir -p /storage/uploads && \ # Executables RUN chmod +x /usr/local/bin/doctor && \ + chmod +x /usr/local/bin/patch-delete-schedule-updated-at-attribute && \ + chmod +x /usr/local/bin/clear-card-cache && \ chmod +x /usr/local/bin/maintenance && \ chmod +x /usr/local/bin/usage && \ chmod +x /usr/local/bin/install && \ diff --git a/app/config/cloud/contributors.json b/app/config/cloud/contributors.json new file mode 100644 index 0000000000..efa36c3cb2 --- /dev/null +++ b/app/config/cloud/contributors.json @@ -0,0 +1 @@ +[1297371,1759475,6360216,20852629,5857008,19310830,9708641,26739219,23742426,22174310,77877486,1477010,29069505,62933155,45863583,42211,176163,58045728,7091609,31401437,1911066,66096031,22432834,43054051,79051850,22895284,100597998,91385411,49699333,7818620,11004008,54898623,27856297,33250853,49375670,30630364,50206,47822499,7481165,52557347,13681567,20492520,51369094,51821861,50256986,28431370,13692220,24373771,15938422,27148250,14805534,36137226,36071208,40193621,56179878,53281158,48085134,19358691,32528768,19733683,37348419,13719696,18309412,42106787,53618500,5306011,4408379,35486736,18586611,15062564,27011453,56090587,44623032,25107942,81188,33922418,16717633,32809211,69401139,25815659,4104127,20889958,3102249,12476526,2635185,28495651,50957556,2791280,31023616,20955511,835733,471907,69008866,44906587,45271396,76054330,45748739,41908747,23402178,47356149,8921,36632821,3668741,71702982,29725587,26303198,785830,51410502,60089135,2847349,43172716,48546075,8216525,41161981,51828039,4334997,80918302,38534289,47860497,80036766,41341387,49818988,58487637,29237374,46913894,5148229,4377199,29686102,26272249,75117692,4090256,27357868,33062368,38664231,46695441,743291,22633385,6368283,11593067,45097959,43381712,3284228,1972717,33012425,61755381,25405707,3144291,44156359,5497267,7423905,20716175,28586681,5975506,23518097,22187384,24191952,7768078,971530,51240166,55633427,34207400,77061285,11719476,35950229,66742927,34406802,802933,50047839,39148877,26602940,9693472,44273767,19362725,31209978,30521594,686298,6237394,35039730,42580581,36671793,8502129,8466918,81866614,54903252,28373606,13381361,72331432,30694270,5355510,8209163,86675510,9453522,42496309,56145786,2149381,393945,22084723,52621436,8872447,5575392,29619660,5547479,8852116,11151445,4717349,17725274,65615065,18537755,29292618,53044263,26597930,10313411,55998629,77529288,17404636,33729848,19422168,17916404,66111735,10329006,33502846,398230,81643826,105039167,47522632,91655303,9774614,10603631,284924,60857954,22885912,116552306,36103454,794606,27729549,1754457,36594527,13899668,78664749,47406531,27698189,5305654,53345517,6756412,29176704,77790497,47504894,37251540,52361778,52200375,1351177,66022861,73975409,25745396,31433638,37118134,43210805,20317665,11923975,47187468,16362381,36751163,14959876,32362757,65529384,52352285,74085816,3628535,43902034,75667593,26132902,466713,617558,96806061,33605526,11290524,43621940,12446314,17146935,55018955,56096559,79797000,40014186,34449936,58387964,23368207,42414965,44056349,33743031,12294525,58251592,33755729,9021747,932084,11428067,97121933,80122730,60894542,58583793,56051809,32243289,9934371,90936802,74638775,65399526,77604,64524822,47782249,43633955,42793632,55969597,72334601,82395440,92818577,60866204,65016769,23725091,45892107,55308895,86314140,82756460,47685349,63562160,73419211,1613216,50882624,91469717,46166258,60927324,41763158,83607556,2171717,50497814,39427312,61322830,40076195,39419448,29397545,55090719,53259730,20885012,64558515,69677883,55741087,72426535,46033036,68477507,30376878,73700530,25518600,29922887,36229969,47573417,40424087,49054503,16880385,22801227,72848513,64347914,814402,49149679,55017867,49481876,67067955,31439735,63878173,80322286,43746210,17332970,22702905,62476876,89888292,75736952,54059881,90782137,63588969,57111920,63330165,70258211,46371923,17837758,59364507,52203828,60147326,18481195,74822422,9803078,67309607,60410049,47360939,19922556,90848252,24698014,58886915,63579762,96648934,68523530,60518745,37345795,3929651,54993657,52061363,43019989,5787917,94674993,71593494,17143469,10288548,1830380,71510505,59124772,2335145,70798495,46474346,49263351,52062536,63151043,65248303,26071571,53626355,43992469,60785452,63467479,71837281,19490891,58628586,38250310,7271718,1110414,57227290,11625672,85063520,88965873,70096901,42029519,85363195,64471630,69353350,66922161,2221746,100430077,12299813,62690310,68282006,99184676,2450,22989561,22212661,59973863,11232940,76688923,22321353,77732479,84286404,32268377,34828782,23068019,57074509,24620969,20735983,26173690,75809937,49760818,86646105,52617262] \ No newline at end of file diff --git a/app/config/cloud/employees.json b/app/config/cloud/employees.json new file mode 100644 index 0000000000..1575a05883 --- /dev/null +++ b/app/config/cloud/employees.json @@ -0,0 +1,33 @@ +{ + "eldad@appwrite.io": { "memberSince": "2020-10-15", "spot": "0", "gitHub": "eldadfux" }, + "christy@appwrite.io": { "memberSince": "2020-12-01", "spot": "1", "gitHub": "christyjacob4" }, + "torsten@appwrite.io": { "memberSince": "2020-12-28", "spot": "2", "gitHub": "torstendittmann" }, + "damodar@appwrite.io": { "memberSince": "2021-01-02", "spot": "3", "gitHub": "lohanidamodar" }, + "bradley@appwrite.io": { "memberSince": "2021-05-21", "spot": "5", "gitHub": "PineappleIOnic" }, + "jake@appwrite.io": { "memberSince": "2021-06-28", "spot": "6", "gitHub": "abnegate" }, + "sara@appwrite.io": { "memberSince": "2021-08-16", "spot": "7", "gitHub": "sarakaandorp" }, + "matej@appwrite.io": { "memberSince": "2021-08-23", "spot": "8", "gitHub": "meldiron" }, + "aditya@appwrite.io": { "memberSince": "2021-09-01", "spot": "9", "gitHub": "adityaoberai" }, + "wess@appwrite.io": { "memberSince": "2021-11-08", "spot": "12", "gitHub": "wess" }, + "may@appwrite.io": { "memberSince": "2021-11-28", "spot": "14", "gitHub": "MayEnder" }, + "elad@appwrite.io": { "memberSince": "2021-12-19", "spot": "15", "gitHub": "elad2412" }, + "vincent@appwrite.io": { "memberSince": "2022-01-01", "spot": "16", "gitHub": "gewenyu99" }, + "haimantika@appwrite.io": { "memberSince": "2022-04-01", "spot": "18", "gitHub": "Haimantika" }, + "chen@appwrite.io": { "memberSince": "2022-01-24", "spot": "19", "gitHub": "chenparnasa" }, + "tessa@appwrite.io": { "memberSince": "2022-04-21", "spot": "20", "gitHub": "tessamero" }, + "shimon@appwrite.io": { "memberSince": "2022-05-01", "spot": "23", "gitHub": "shimonewman" }, + "shmuel@appwrite.io": { "memberSince": "2022-03-20", "spot": "24", "gitHub": "fogelito" }, + "arman@appwrite.io": { "memberSince": "2022-04-04", "spot": "25", "gitHub": "ArmanNik" }, + "carla@appwrite.io": { "memberSince": "2022-04-04", "spot": "26", "gitHub": "heyCarla" }, + "emma@appwrite.io": { "memberSince": "2022-05-08", "spot": "27", "gitHub": "emmacarpagnano1" }, + "dylan@appwrite.io": { "memberSince": "2022-05-09", "spot": "28", "gitHub": "DylanG-64" }, + "steven@appwrite.io": { "memberSince": "2022-07-01", "spot": "30", "gitHub": "stnguyen90" }, + "jyoti@appwrite.io": { "memberSince": "2022-10-24", "spot": "31", "gitHub": "joeyouss" }, + "jade@appwrite.io": { "memberSince": "2022-10-31", "spot": "32", "gitHub": "dajebp" }, + "khushboo@appwrite.io": { "memberSince": "2021-11-08", "spot": "13", "gitHub": "vermakhushboo" }, + "thomas@appwrite.io": { "memberSince": "2022-11-03", "spot": "34", "gitHub": "TGlide" }, + "holly@appwrite.io": { "memberSince": "2022-12-05", "spot": "35", "gitHub": "HollyBarclay" }, + "laura@appwrite.io": { "memberSince": "2023-01-25", "spot": "36", "gitHub": "LauraDuRy" }, + "caio@appwrite.io": { "memberSince": "2023-03-27", "spot": "37", "gitHub": "ariascaio" }, + "luke@appwrite.io": { "memberSince": "2023-05-04", "spot": "38", "gitHub": "loks0n" } +} diff --git a/app/config/cloud/heroes.json b/app/config/cloud/heroes.json new file mode 100644 index 0000000000..3fc81b3b3f --- /dev/null +++ b/app/config/cloud/heroes.json @@ -0,0 +1,9 @@ +{ + "bishwajeet.techmaster@gmail.com": { "memberSince": "2023-02-07" }, + "lucasaudart@gmail.com": { "memberSince": "2023-02-07" }, + "tkarmakar27112000@gmail.com": { "memberSince": "2023-02-07" }, + "alves.mckl@gmail.com": { "memberSince": "2023-02-07" }, + "dpns_nampula@rnlay.com": { "memberSince": "2023-02-07" }, + "a.stephensimon@outlook.com": { "memberSince": "2023-02-07" }, + "hidianapham@gmail.com": { "memberSince": "2023-02-07" } +} diff --git a/app/config/collections.php b/app/config/collections.php index 3f56c48e87..338cee0e34 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -2356,17 +2356,6 @@ $collections = [ 'array' => false, 'filters' => [], ], - [ - '$id' => ID::custom('scheduleUpdatedAt'), - 'type' => Database::VAR_DATETIME, - 'format' => '', - 'size' => 0, - 'signed' => false, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => ['datetime'], - ], [ '$id' => ID::custom('timeout'), 'type' => Database::VAR_INTEGER, diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index d5d9539a5f..7c46e634c1 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -566,6 +566,10 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $name = $oauth2->getUserName($accessToken); $email = $oauth2->getUserEmail($accessToken); + if (empty($email)) { + throw new Exception(Exception::USER_UNAUTHORIZED, 'OAuth provider failed to return email.'); + } + /** * Is verified is not used yet, since we don't know after an accout is created anymore if it was verified or not. */ diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index fbc77d69f6..14462d34e8 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -7,9 +7,16 @@ use Appwrite\Utopia\Response; use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QROptions; use Utopia\App; +use Utopia\CLI\Console; use Utopia\Config\Config; +use Utopia\Database\Database; +use Utopia\Database\DateTime; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\UID; use Utopia\Image\Image; +use Utopia\Logger\Log; +use Utopia\Logger\Logger; use Utopia\Validator\Boolean; use Utopia\Validator\HexColor; use Utopia\Validator\Range; @@ -49,11 +56,139 @@ $avatarCallback = function (string $type, string $code, int $width, int $height, $response ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT') ->setContentType('image/png') - ->file($data) - ; + ->file($data); unset($image); }; +$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForConsole, ?Logger $logger) { + try { + $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); + + $sessions = $user->getAttribute('sessions', []); + + $gitHubSession = null; + foreach ($sessions as $session) { + if ($session->getAttribute('provider', '') === 'github') { + $gitHubSession = $session; + break; + } + } + + if (empty($gitHubSession)) { + throw new Exception(Exception::GENERAL_UNKNOWN, 'GitHub session not found.'); + } + + $provider = $gitHubSession->getAttribute('provider', ''); + $accessToken = $gitHubSession->getAttribute('providerAccessToken'); + $accessTokenExpiry = $gitHubSession->getAttribute('providerAccessTokenExpiry'); + $refreshToken = $gitHubSession->getAttribute('providerRefreshToken'); + + $appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? ''; + $appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}'; + + $className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider); + + if (!\class_exists($className)) { + throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED); + } + + $oauth2 = new $className($appId, $appSecret, '', [], []); + + $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); + if ($isExpired) { + try { + $oauth2->refreshTokens($refreshToken); + + $accessToken = $oauth2->getAccessToken(''); + $refreshToken = $oauth2->getRefreshToken(''); + + $verificationId = $oauth2->getUserID($accessToken); + + if (empty($verificationId)) { + throw new \Exception("Locked tokens."); // Race codition, handeled in catch + } + + $gitHubSession + ->setAttribute('providerAccessToken', $accessToken) + ->setAttribute('providerRefreshToken', $refreshToken) + ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry(''))); + + Authorization::skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession)); + + $dbForProject->deleteCachedDocument('users', $user->getId()); + } catch (Throwable $err) { + $index = 0; + do { + $previousAccessToken = $gitHubSession->getAttribute('providerAccessToken'); + + $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); + $sessions = $user->getAttribute('sessions', []); + + $gitHubSession = new Document(); + foreach ($sessions as $session) { + if ($session->getAttribute('provider', '') === 'github') { + $gitHubSession = $session; + break; + } + } + + $accessToken = $gitHubSession->getAttribute('providerAccessToken'); + + if ($accessToken !== $previousAccessToken) { + break; + } + + $index++; + \usleep(500000); + } while ($index < 10); + } + } + + $oauth2 = new $className($appId, $appSecret, '', [], []); + $githubUser = $oauth2->getUserSlug($accessToken); + $githubId = $oauth2->getUserID($accessToken); + + return [ + 'name' => $githubUser, + 'id' => $githubId + ]; + } catch (Exception $error) { + if ($logger) { + $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); + + $log = new Log(); + $log->setNamespace('console'); + $log->setServer(\gethostname()); + $log->setVersion($version); + $log->setType(Log::TYPE_ERROR); + $log->setMessage($error->getMessage()); + + $log->addTag('code', $error->getCode()); + $log->addTag('verboseType', get_class($error)); + + $log->addExtra('file', $error->getFile()); + $log->addExtra('line', $error->getLine()); + $log->addExtra('trace', $error->getTraceAsString()); + $log->addExtra('detailedTrace', $error->getTrace()); + + $log->setAction('avatarsGetGitHub'); + + $isProduction = App::getEnv('_APP_ENV', 'development') === 'production'; + $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); + + $responseCode = $logger->addLog($log); + Console::info('GitHub error log pushed with status code: ' . $responseCode); + } + + Console::warning("Failed: {$error->getMessage()}"); + Console::warning($error->getTraceAsString()); + + return []; + } + + return []; +}; + App::get('/v1/avatars/credit-cards/:code') ->desc('Get Credit Card Icon') ->groups(['api', 'avatars']) @@ -160,8 +295,7 @@ App::get('/v1/avatars/image') $response ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT') ->setContentType('image/png') - ->file($data) - ; + ->file($data); unset($image); }); @@ -274,8 +408,7 @@ App::get('/v1/avatars/favicon') $response ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT') ->setContentType('image/x-icon') - ->file($data) - ; + ->file($data); } $fetch = @\file_get_contents($outputHref, false); @@ -292,8 +425,7 @@ App::get('/v1/avatars/favicon') $response ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT') ->setContentType('image/png') - ->file($data) - ; + ->file($data); unset($image); }); @@ -334,8 +466,7 @@ App::get('/v1/avatars/qr') $response ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache ->setContentType('image/png') - ->send($image->output('png', 9)) - ; + ->send($image->output('png', 9)); }); App::get('/v1/avatars/initials') @@ -419,6 +550,680 @@ App::get('/v1/avatars/initials') $response ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache ->setContentType('image/png') - ->file($image->getImageBlob()) - ; + ->file($image->getImageBlob()); + }); + +App::get('/v1/cards/cloud') + ->desc('Get Front Of Cloud Card') + ->groups(['api', 'avatars']) + ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resourceType', 'cards/cloud') + ->label('cache.resource', 'card/{request.userId}') + ->label('docs', false) + ->label('origin', '*') + ->param('userId', '', new UID(), 'User ID.', true) + ->param('mock', '', new WhiteList(['employee', 'employee-2digit', 'hero', 'contributor', 'normal', 'platinum', 'normal-no-github', 'normal-long']), 'Mocking behaviour.', true) + ->param('width', 0, new Range(0, 512), 'Resize image width, Pass an integer between 0 to 512.', true) + ->param('height', 0, new Range(0, 320), 'Resize image height, Pass an integer between 0 to 320.', true) + ->inject('user') + ->inject('project') + ->inject('dbForProject') + ->inject('dbForConsole') + ->inject('response') + ->inject('heroes') + ->inject('contributors') + ->inject('employees') + ->inject('logger') + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { + $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); + + if ($user->isEmpty() && empty($mock)) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + if (!$mock) { + $name = $user->getAttribute('name', 'Anonymous'); + $email = $user->getAttribute('email', ''); + $createdAt = new \DateTime($user->getCreatedAt()); + + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger); + $githubName = $gitHub['name'] ?? ''; + $githubId = $gitHub['id'] ?? ''; + + $isHero = \array_key_exists($email, $heroes); + $isContributor = \in_array($githubId, $contributors); + $isEmployee = \array_key_exists($email, $employees); + $employeeNumber = $isEmployee ? $employees[$email]['spot'] : ''; + + if ($isHero) { + $createdAt = new \DateTime($heroes[$email]['memberSince'] ?? ''); + } elseif ($isEmployee) { + $createdAt = new \DateTime($employees[$email]['memberSince'] ?? ''); + } + + if (!$isEmployee && !empty($githubName)) { + $employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub']) ?? '', $employees)); + if (!empty($employeeGitHub)) { + $isEmployee = true; + $employeeNumber = $isEmployee ? $employees[$employeeGitHub]['spot'] : ''; + $createdAt = new \DateTime($employees[$employeeGitHub]['memberSince'] ?? ''); + } + } + + $isPlatinum = $user->getInternalId() % 100 === 0; + } else { + $name = $mock === 'normal-long' ? 'Sir First Walter O\'Brian Junior' : 'Walter O\'Brian'; + $createdAt = new \DateTime('now'); + $githubName = $mock === 'normal-no-github' ? '' : ($mock === 'normal-long' ? 'sir-first-walterobrian-junior' : 'walterobrian'); + $isHero = $mock === 'hero'; + $isContributor = $mock === 'contributor'; + $isEmployee = \str_starts_with($mock, 'employee'); + $employeeNumber = match ($mock) { + 'employee' => '1', + 'employee-2digit' => '18', + default => '' + }; + + $isPlatinum = $mock === 'platinum'; + } + + if ($isEmployee) { + $isContributor = false; + $isHero = false; + } + + if ($isHero) { + $isContributor = false; + $isEmployee = false; + } + + if ($isContributor) { + $isHero = false; + $isEmployee = false; + } + + $isGolden = $isEmployee || $isHero || $isContributor; + $isPlatinum = $isGolden ? false : $isPlatinum; + $memberSince = \strtoupper('Member since ' . $createdAt->format('M') . ' ' . $createdAt->format('d') . ', ' . $createdAt->format('o')); + + $imagePath = $isGolden ? 'front-golden.png' : ($isPlatinum ? 'front-platinum.png' : 'front.png'); + + $baseImage = new \Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $imagePath); + + if ($isEmployee) { + $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/employee.png'); + $image->setGravity(Imagick::GRAVITY_CENTER); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 35); + + $text = new \ImagickDraw(); + $text->setTextAlignment(Imagick::ALIGN_CENTER); + $text->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf'); + $text->setFillColor(new \ImagickPixel('#FFFADF')); + $text->setFontSize(\strlen($employeeNumber) <= 2 ? 54 : 48); + $text->setFontWeight(700); + $metricsText = $baseImage->queryFontMetrics($text, $employeeNumber); + + $hashtag = new \ImagickDraw(); + $hashtag->setTextAlignment(Imagick::ALIGN_CENTER); + $hashtag->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf'); + $hashtag->setFillColor(new \ImagickPixel('#FFFADF')); + $hashtag->setFontSize(28); + $hashtag->setFontWeight(700); + $metricsHashtag = $baseImage->queryFontMetrics($hashtag, '#'); + + $startX = 898; + $totalWidth = $metricsHashtag['textWidth'] + 12 + $metricsText['textWidth']; + + $hashtagX = ($metricsHashtag['textWidth'] / 2); + $textX = $hashtagX + 12 + ($metricsText['textWidth'] / 2); + + $hashtagX -= $totalWidth / 2; + $textX -= $totalWidth / 2; + + $hashtagX += $startX; + $textX += $startX; + + $baseImage->annotateImage($hashtag, $hashtagX, 150, 0, '#'); + $baseImage->annotateImage($text, $textX, 150, 0, $employeeNumber); + } + + if ($isContributor) { + $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/contributor.png'); + $image->setGravity(Imagick::GRAVITY_CENTER); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 34); + } + + if ($isHero) { + $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/hero.png'); + $image->setGravity(Imagick::GRAVITY_CENTER); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 34); + } + + setlocale(LC_ALL, "en_US.utf8"); + // $name = \iconv("utf-8", "ascii//TRANSLIT", $name); + // $memberSince = \iconv("utf-8", "ascii//TRANSLIT", $memberSince); + // $githubName = \iconv("utf-8", "ascii//TRANSLIT", $githubName); + + $text = new \ImagickDraw(); + $text->setTextAlignment(Imagick::ALIGN_CENTER); + $text->setFont(__DIR__ . '/../../../public/fonts/Poppins-Bold.ttf'); + $text->setFillColor(new \ImagickPixel('#FFFFFF')); + + if (\strlen($name) > 32) { + $name = \substr($name, 0, 32); + } + + if (\strlen($name) <= 23) { + $text->setFontSize(80); + $scalingDown = false; + } else { + $text->setFontSize(54); + $scalingDown = true; + } + $text->setFontWeight(700); + $baseImage->annotateImage($text, 512, 477, 0, $name); + + $text = new \ImagickDraw(); + $text->setTextAlignment(Imagick::ALIGN_CENTER); + $text->setFont(__DIR__ . '/../../../public/fonts/Inter-SemiBold.ttf'); + $text->setFillColor(new \ImagickPixel($isGolden || $isPlatinum ? '#FFFFFF' : '#FFB9CC')); + $text->setFontSize(27); + $text->setFontWeight(600); + $text->setTextKerning(1.08); + $baseImage->annotateImage($text, 512, 541, 0, \strtoupper($memberSince)); + + if (!empty($githubName)) { + $text = new \ImagickDraw(); + $text->setTextAlignment(Imagick::ALIGN_CENTER); + $text->setFont(__DIR__ . '/../../../public/fonts/Inter-Regular.ttf'); + $text->setFillColor(new \ImagickPixel('#FFFFFF')); + $text->setFontSize($scalingDown ? 28 : 32); + $text->setFontWeight(400); + $metrics = $baseImage->queryFontMetrics($text, $githubName); + + $baseImage->annotateImage($text, 512 + 20 + 4, 373 + ($scalingDown ? 2 : 0), 0, $githubName); + + $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/github.png'); + $image->setGravity(Imagick::GRAVITY_CENTER); + $precisionFix = 5; + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 512 - ($metrics['textWidth'] / 2) - 20 - 4, 373 - ($metrics['textHeight'] - $precisionFix)); + } + + if (!empty($width) || !empty($height)) { + $baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1); + } + + $response + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache + ->setContentType('image/png') + ->file($baseImage->getImageBlob()); + }); + +App::get('/v1/cards/cloud-back') + ->desc('Get Back Of Cloud Card') + ->groups(['api', 'avatars']) + ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resourceType', 'cards/cloud-back') + ->label('cache.resource', 'card-back/{request.userId}') + ->label('docs', false) + ->label('origin', '*') + ->param('userId', '', new UID(), 'User ID.', true) + ->param('mock', '', new WhiteList(['golden', 'normal', 'platinum']), 'Mocking behaviour.', true) + ->param('width', 0, new Range(0, 512), 'Resize image width, Pass an integer between 0 to 512.', true) + ->param('height', 0, new Range(0, 320), 'Resize image height, Pass an integer between 0 to 320.', true) + ->inject('user') + ->inject('project') + ->inject('dbForProject') + ->inject('dbForConsole') + ->inject('response') + ->inject('heroes') + ->inject('contributors') + ->inject('employees') + ->inject('logger') + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { + $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); + + if ($user->isEmpty() && empty($mock)) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + if (!$mock) { + $userId = $user->getId(); + $email = $user->getAttribute('email', ''); + + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger); + $githubId = $gitHub['id'] ?? ''; + + $isHero = \array_key_exists($email, $heroes); + $isContributor = \in_array($githubId, $contributors); + $isEmployee = \array_key_exists($email, $employees); + + $isGolden = $isEmployee || $isHero || $isContributor; + $isPlatinum = $user->getInternalId() % 100 === 0; + } else { + $userId = '63e0bcf3c3eb803ba530'; + + $isGolden = $mock === 'golden'; + $isPlatinum = $mock === 'platinum'; + } + + $userId = 'UID ' . $userId; + + $isPlatinum = $isGolden ? false : $isPlatinum; + + $imagePath = $isGolden ? 'back-golden.png' : ($isPlatinum ? 'back-platinum.png' : 'back.png'); + + $baseImage = new \Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $imagePath); + + setlocale(LC_ALL, "en_US.utf8"); + // $userId = \iconv("utf-8", "ascii//TRANSLIT", $userId); + + $text = new \ImagickDraw(); + $text->setTextAlignment(Imagick::ALIGN_CENTER); + $text->setFont(__DIR__ . '/../../../public/fonts/SourceCodePro-Regular.ttf'); + $text->setFillColor(new \ImagickPixel($isGolden ? '#664A1E' : ($isPlatinum ? '#555555' : '#E8E9F0'))); + $text->setFontSize(28); + $text->setFontWeight(400); + $baseImage->annotateImage($text, 512, 596, 0, $userId); + + if (!empty($width) || !empty($height)) { + $baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1); + } + + $response + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache + ->setContentType('image/png') + ->file($baseImage->getImageBlob()); + }); + +App::get('/v1/cards/cloud-og') + ->desc('Get OG Image From Cloud Card') + ->groups(['api', 'avatars']) + ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resourceType', 'cards/cloud-og') + ->label('cache.resource', 'card-og/{request.userId}') + ->label('docs', false) + ->label('origin', '*') + ->param('userId', '', new UID(), 'User ID.', true) + ->param('mock', '', new WhiteList(['employee', 'employee-2digit', 'hero', 'contributor', 'normal', 'platinum', 'normal-no-github', 'normal-long', 'normal-long-right', 'normal-long-middle', 'normal-bg2', 'normal-bg3', 'normal-right', 'normal-middle', 'platinum-right', 'platinum-middle', 'hero-middle', 'hero-right', 'contributor-right', 'employee-right', 'contributor-middle', 'employee-middle', 'employee-2digit-middle', 'employee-2digit-right']), 'Mocking behaviour.', true) + ->param('width', 0, new Range(0, 1024), 'Resize image card width, Pass an integer between 0 to 1024.', true) + ->param('height', 0, new Range(0, 1024), 'Resize image card height, Pass an integer between 0 to 1024.', true) + ->inject('user') + ->inject('project') + ->inject('dbForProject') + ->inject('dbForConsole') + ->inject('response') + ->inject('heroes') + ->inject('contributors') + ->inject('employees') + ->inject('logger') + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { + $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); + + if ($user->isEmpty() && empty($mock)) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + if (!$mock) { + $internalId = $user->getInternalId(); + $bgVariation = $internalId % 3 === 0 ? '1' : ($internalId % 3 === 1 ? '2' : '3'); + $cardVariation = $internalId % 3 === 0 ? '1' : ($internalId % 3 === 1 ? '2' : '3'); + + $name = $user->getAttribute('name', 'Anonymous'); + $email = $user->getAttribute('email', ''); + $createdAt = new \DateTime($user->getCreatedAt()); + + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger); + $githubName = $gitHub['name'] ?? ''; + $githubId = $gitHub['id'] ?? ''; + + $isHero = \array_key_exists($email, $heroes); + $isContributor = \in_array($githubId, $contributors); + $isEmployee = \array_key_exists($email, $employees); + $employeeNumber = $isEmployee ? $employees[$email]['spot'] : ''; + + if ($isHero) { + $createdAt = new \DateTime($heroes[$email]['memberSince'] ?? ''); + } elseif ($isEmployee) { + $createdAt = new \DateTime($employees[$email]['memberSince'] ?? ''); + } + + if (!$isEmployee && !empty($githubName)) { + $employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub']) ?? '', $employees)); + if (!empty($employeeGitHub)) { + $isEmployee = true; + $employeeNumber = $isEmployee ? $employees[$employeeGitHub]['spot'] : ''; + $createdAt = new \DateTime($employees[$employeeGitHub]['memberSince'] ?? ''); + } + } + + $isPlatinum = $user->getInternalId() % 100 === 0; + } else { + $bgVariation = \str_ends_with($mock, '-bg2') ? '2' : (\str_ends_with($mock, '-bg3') ? '3' : '1'); + $cardVariation = \str_ends_with($mock, '-right') ? '2' : (\str_ends_with($mock, '-middle') ? '3' : '1'); + $name = \str_starts_with($mock, 'normal-long') ? 'Sir First Walter O\'Brian Junior' : 'Walter O\'Brian'; + $createdAt = new \DateTime('now'); + $githubName = $mock === 'normal-no-github' ? '' : (\str_starts_with($mock, 'normal-long') ? 'sir-first-walterobrian-junior' : 'walterobrian'); + $isHero = \str_starts_with($mock, 'hero'); + $isContributor = \str_starts_with($mock, 'contributor'); + $isEmployee = \str_starts_with($mock, 'employee'); + $employeeNumber = match ($mock) { + 'employee' => '1', + 'employee-right' => '1', + 'employee-middle' => '1', + 'employee-2digit' => '18', + 'employee-2digit-right' => '18', + 'employee-2digit-middle' => '18', + default => '' + }; + + $isPlatinum = \str_starts_with($mock, 'platinum'); + } + + if ($isEmployee) { + $isContributor = false; + $isHero = false; + } + + if ($isHero) { + $isContributor = false; + $isEmployee = false; + } + + if ($isContributor) { + $isHero = false; + $isEmployee = false; + } + + $isGolden = $isEmployee || $isHero || $isContributor; + $isPlatinum = $isGolden ? false : $isPlatinum; + $memberSince = \strtoupper('Member since ' . $createdAt->format('M') . ' ' . $createdAt->format('d') . ', ' . $createdAt->format('o')); + + $baseImage = new \Imagick(__DIR__ . "/../../../public/images/cards/cloud/og-background{$bgVariation}.png"); + + $cardType = $isGolden ? '-golden' : ($isPlatinum ? '-platinum' : ''); + + $image = new Imagick(__DIR__ . "/../../../public/images/cards/cloud/og-card{$cardType}{$cardVariation}.png"); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 1008 / 2 - $image->getImageWidth() / 2, 1008 / 2 - $image->getImageHeight() / 2); + + $imageLogo = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/og-background-logo.png'); + $imageShadow = new Imagick(__DIR__ . "/../../../public/images/cards/cloud/og-shadow{$cardType}.png"); + if ($cardVariation === '1') { + $baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 32, 1008 - $imageLogo->getImageHeight() - 32); + $baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -450, 700); + } elseif ($cardVariation === '2') { + $baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 1008 - $imageLogo->getImageWidth() - 32, 1008 - $imageLogo->getImageHeight() - 32); + $baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -20, 710); + } else { + $baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 1008 - $imageLogo->getImageWidth() - 32, 1008 - $imageLogo->getImageHeight() - 32); + $baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -135, 710); + } + + if ($isEmployee) { + $file = $cardVariation === '3' ? 'employee-skew.png' : 'employee.png'; + $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $file); + $image->setGravity(Imagick::GRAVITY_CENTER); + + $hashtag = new \ImagickDraw(); + $hashtag->setTextAlignment(Imagick::ALIGN_LEFT); + $hashtag->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf'); + $hashtag->setFillColor(new \ImagickPixel('#FFFADF')); + $hashtag->setFontSize(20); + $hashtag->setFontWeight(700); + + $text = new \ImagickDraw(); + $text->setTextAlignment(Imagick::ALIGN_LEFT); + $text->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf'); + $text->setFillColor(new \ImagickPixel('#FFFADF')); + $text->setFontSize(\strlen($employeeNumber) <= 1 ? 36 : 28); + $text->setFontWeight(700); + + if ($cardVariation === '3') { + $hashtag->setFontSize(16); + $text->setFontSize(\strlen($employeeNumber) <= 1 ? 30 : 26); + + $hashtag->skewY(20); + $hashtag->skewX(20); + $text->skewY(20); + $text->skewX(20); + } + + $metricsHashtag = $baseImage->queryFontMetrics($hashtag, '#'); + $metricsText = $baseImage->queryFontMetrics($text, $employeeNumber); + + $group = new Imagick(); + $groupWidth = $metricsHashtag['textWidth'] + 6 + $metricsText['textWidth']; + + if ($cardVariation === '1') { + $group->newImage($groupWidth, $metricsText['textHeight'], '#00000000'); + $group->annotateImage($hashtag, 0, $metricsText['textHeight'], 0, '#'); + $group->annotateImage($text, $metricsHashtag['textWidth'] + 6, $metricsText['textHeight'], 0, $employeeNumber); + + $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); + $image->rotateImage(new ImagickPixel('#00000000'), -20); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203); + + $group->rotateImage(new ImagickPixel('#00000000'), -22); + + if (\strlen($employeeNumber) <= 1) { + $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 660, 245); + } else { + $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 655, 247); + } + } elseif ($cardVariation === '2') { + $group->newImage($groupWidth, $metricsText['textHeight'], '#00000000'); + $group->annotateImage($hashtag, 0, $metricsText['textHeight'], 0, '#'); + $group->annotateImage($text, $metricsHashtag['textWidth'] + 6, $metricsText['textHeight'], 0, $employeeNumber); + + $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); + $image->rotateImage(new ImagickPixel('#00000000'), 30); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425); + + $group->rotateImage(new ImagickPixel('#00000000'), 32); + + if (\strlen($employeeNumber) <= 1) { + $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 775, 465); + } else { + $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 767, 470); + } + } else { + $group->newImage(300, 300, '#00000000'); + + $hashtag->annotation(0, $metricsText['textHeight'], '#'); + $text->annotation($metricsHashtag['textWidth'] + 2, $metricsText['textHeight'], $employeeNumber); + + $group->drawImage($hashtag); + $group->drawImage($text); + + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293); + + if (\strlen($employeeNumber) <= 1) { + $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 670, 317); + } else { + $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 663, 322); + } + } + } + + if ($isContributor) { + $file = $cardVariation === '3' ? 'contributor-skew.png' : 'contributor.png'; + $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $file); + $image->setGravity(Imagick::GRAVITY_CENTER); + + if ($cardVariation === '1') { + $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); + $image->rotateImage(new ImagickPixel('#00000000'), -20); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203); + } elseif ($cardVariation === '2') { + $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); + $image->rotateImage(new ImagickPixel('#00000000'), 30); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425); + } else { + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293); + } + } + + if ($isHero) { + $file = $cardVariation === '3' ? 'hero-skew.png' : 'hero.png'; + $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $file); + $image->setGravity(Imagick::GRAVITY_CENTER); + + if ($cardVariation === '1') { + $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); + $image->rotateImage(new ImagickPixel('#00000000'), -20); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203); + } elseif ($cardVariation === '2') { + $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); + $image->rotateImage(new ImagickPixel('#00000000'), 30); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425); + } else { + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293); + } + } + + setlocale(LC_ALL, "en_US.utf8"); + // $name = \iconv("utf-8", "ascii//TRANSLIT", $name); + // $memberSince = \iconv("utf-8", "ascii//TRANSLIT", $memberSince); + // $githubName = \iconv("utf-8", "ascii//TRANSLIT", $githubName); + + $textName = new \ImagickDraw(); + $textName->setTextAlignment(Imagick::ALIGN_CENTER); + $textName->setFont(__DIR__ . '/../../../public/fonts/Poppins-Bold.ttf'); + $textName->setFillColor(new \ImagickPixel('#FFFFFF')); + + if (\strlen($name) > 32) { + $name = \substr($name, 0, 32); + } + + if ($cardVariation === '1') { + if (\strlen($name) <= 23) { + $scalingDown = false; + $textName->setFontSize(54); + } else { + $scalingDown = true; + $textName->setFontSize(36); + } + } elseif ($cardVariation === '2') { + if (\strlen($name) <= 23) { + $scalingDown = false; + $textName->setFontSize(50); + } else { + $scalingDown = true; + $textName->setFontSize(34); + } + } else { + if (\strlen($name) <= 23) { + $scalingDown = false; + $textName->setFontSize(44); + } else { + $scalingDown = true; + $textName->setFontSize(32); + } + } + + $textName->setFontWeight(700); + + $textMember = new \ImagickDraw(); + $textMember->setTextAlignment(Imagick::ALIGN_CENTER); + $textMember->setFont(__DIR__ . '/../../../public/fonts/Inter-Medium.ttf'); + $textMember->setFillColor(new \ImagickPixel($isGolden || $isPlatinum ? '#FFFFFF' : '#FFB9CC')); + $textMember->setFontWeight(500); + $textMember->setTextKerning(1.12); + + if ($cardVariation === '1') { + $textMember->setFontSize(21); + + $baseImage->annotateImage($textName, 550, 600, -22, $name); + $baseImage->annotateImage($textMember, 585, 635, -22, $memberSince); + } elseif ($cardVariation === '2') { + $textMember->setFontSize(20); + + $baseImage->annotateImage($textName, 435, 590, 31.37, $name); + $baseImage->annotateImage($textMember, 412, 628, 31.37, $memberSince); + } else { + $textMember->setFontSize(16); + + $textName->skewY(20); + $textName->skewX(20); + $textName->annotation(320, 700, $name); + + $textMember->skewY(20); + $textMember->skewX(20); + $textMember->annotation(330, 735, $memberSince); + + $baseImage->drawImage($textName); + $baseImage->drawImage($textMember); + } + + if (!empty($githubName)) { + $text = new \ImagickDraw(); + $text->setTextAlignment(Imagick::ALIGN_LEFT); + $text->setFont(__DIR__ . '/../../../public/fonts/Inter-Regular.ttf'); + $text->setFillColor(new \ImagickPixel('#FFFFFF')); + $text->setFontSize($scalingDown ? 16 : 20); + $text->setFontWeight(400); + + if ($cardVariation === '1') { + $metrics = $baseImage->queryFontMetrics($text, $githubName); + + $group = new Imagick(); + $groupWidth = $metrics['textWidth'] + 32 + 4; + $group->newImage($groupWidth, $metrics['textHeight'] + 10, '#00000000'); + $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/github.png'); + $image->setGravity(Imagick::GRAVITY_CENTER); + $image->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1); + $precisionFix = -1; + + $group->compositeImage($image, Imagick::COMPOSITE_OVER, 0, 0); + $group->annotateImage($text, 32 + 4, $metrics['textHeight'] - $precisionFix, 0, $githubName); + + $group->rotateImage(new ImagickPixel('#00000000'), -22); + $x = 510 - $group->getImageWidth() / 2; + $y = 530 - $group->getImageHeight() / 2; + $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, $x, $y); + } elseif ($cardVariation === '2') { + $metrics = $baseImage->queryFontMetrics($text, $githubName); + + $group = new Imagick(); + $groupWidth = $metrics['textWidth'] + 32 + 4; + $group->newImage($groupWidth, $metrics['textHeight'] + 10, '#00000000'); + $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/github.png'); + $image->setGravity(Imagick::GRAVITY_CENTER); + $image->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1); + $precisionFix = -1; + + $group->compositeImage($image, Imagick::COMPOSITE_OVER, 0, 0); + $group->annotateImage($text, 32 + 4, $metrics['textHeight'] - $precisionFix, 0, $githubName); + + $group->rotateImage(new ImagickPixel('#00000000'), 31.11); + $x = 485 - $group->getImageWidth() / 2; + $y = 530 - $group->getImageHeight() / 2; + $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, $x, $y); + } else { + $text->skewY(20); + $text->skewX(20); + $text->setTextAlignment(\Imagick::ALIGN_CENTER); + + $text->annotation(320 + 15 + 2, 640, $githubName); + $metrics = $baseImage->queryFontMetrics($text, $githubName); + + $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/github-skew.png'); + $image->setGravity(Imagick::GRAVITY_CENTER); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 512 - ($metrics['textWidth'] / 2), 518 + \strlen($githubName) * 1.3); + + $baseImage->drawImage($text); + } + } + + if (!empty($width) || !empty($height)) { + $baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1); + } + + $response + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache + ->setContentType('image/png') + ->file($baseImage->getImageBlob()); }); diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index f0cafe3c77..270823b490 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -93,7 +93,6 @@ App::post('/v1/functions') 'deployment' => '', 'events' => $events, 'schedule' => $schedule, - 'scheduleUpdatedAt' => DateTime::now(), 'timeout' => $timeout, 'entrypoint' => $entrypoint, 'buildCommand' => $buildCommand, @@ -544,7 +543,6 @@ App::put('/v1/functions/:functionId') 'name' => $name, 'events' => $events, 'schedule' => $schedule, - 'scheduleUpdatedAt' => DateTime::now(), 'timeout' => $timeout, 'enabled' => $enabled, 'logging' => $logging, @@ -556,18 +554,11 @@ App::put('/v1/functions/:functionId') $schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId')); - /** - * In case we want to clear the schedule - */ - if (!empty($function->getAttribute('deployment'))) { - $schedule->setAttribute('resourceUpdatedAt', $function->getAttribute('scheduleUpdatedAt')); - } - $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $eventsInstance->setParam('functionId', $function->getId()); @@ -624,15 +615,10 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId') ]))); $schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId')); - - $active = !empty($function->getAttribute('schedule')); - - if ($active) { - $schedule->setAttribute('resourceUpdatedAt', datetime::now()); - } - - $schedule->setAttribute('active', $active); - + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('schedule', $function->getAttribute('schedule')) + ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $events @@ -881,22 +867,6 @@ App::post('/v1/functions/:functionId/deployments') } } - /** - * TODO Should we update also the function collection with the scheduleUpdatedAt attr? - */ - - $schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId')); - - $active = !empty($function->getAttribute('schedule')); - - if ($active) { - $schedule->setAttribute('resourceUpdatedAt', datetime::now()); - } - - $schedule->setAttribute('active', $active); - - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); - $metadata = null; $events @@ -1531,7 +1501,8 @@ App::post('/v1/functions/:functionId/variables') ->inject('project') ->inject('response') ->inject('dbForProject') - ->action(function (string $functionId, string $key, string $value, Document $project, Response $response, Database $dbForProject) { + ->inject('dbForConsole') + ->action(function (string $functionId, string $key, string $value, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -1561,6 +1532,13 @@ App::post('/v1/functions/:functionId/variables') throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); } + $schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId')); + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('schedule', $function->getAttribute('schedule')) + ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); + Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $dbForProject->deleteCachedDocument('functions', $function->getId()); // Stop all running runtimes with this variable @@ -1657,7 +1635,8 @@ App::put('/v1/functions/:functionId/variables/:variableId') ->inject('project') ->inject('response') ->inject('dbForProject') - ->action(function (string $functionId, string $variableId, string $key, ?string $value, Document $project, Response $response, Database $dbForProject) { + ->inject('dbForConsole') + ->action(function (string $functionId, string $variableId, string $key, ?string $value, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) { $function = $dbForProject->getDocument('functions', $functionId); @@ -1686,6 +1665,13 @@ App::put('/v1/functions/:functionId/variables/:variableId') throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); } + $schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId')); + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('schedule', $function->getAttribute('schedule')) + ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); + Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $dbForProject->deleteCachedDocument('functions', $function->getId()); // Stop all running runtimes with this variable @@ -1715,7 +1701,8 @@ App::delete('/v1/functions/:functionId/variables/:variableId') ->inject('project') ->inject('response') ->inject('dbForProject') - ->action(function (string $functionId, string $variableId, Document $project, Response $response, Database $dbForProject) { + ->inject('dbForConsole') + ->action(function (string $functionId, string $variableId, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -1733,6 +1720,14 @@ App::delete('/v1/functions/:functionId/variables/:variableId') } $dbForProject->deleteDocument('variables', $variable->getId()); + + $schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId')); + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('schedule', $function->getAttribute('schedule')) + ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); + Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $dbForProject->deleteCachedDocument('functions', $function->getId()); // Stop all running runtimes with this variable diff --git a/app/controllers/web/console.php b/app/controllers/web/console.php index 8115f09285..ecd86e84b1 100644 --- a/app/controllers/web/console.php +++ b/app/controllers/web/console.php @@ -1,5 +1,6 @@ groups(['web']) ->label('permission', 'public') ->label('scope', 'home') + ->inject('request') ->inject('response') - ->action(function (Response $response) { + ->action(function (Request $request, Response $response) { $fallback = file_get_contents(__DIR__ . '/../../../console/index.html'); + + // Card SSR + if (\str_starts_with($request->getURI(), '/card')) { + $urlCunks = \explode('/', $request->getURI()); + $userId = $urlCunks[\count($urlCunks) - 1] ?? ''; + + $domain = $request->getProtocol() . '://' . $request->getHostname(); + + if (!empty($userId)) { + $ogImageUrl = $domain . '/v1/cards/cloud-og?userId=' . $userId; + } else { + $ogImageUrl = $domain . '/v1/cards/cloud-og?mock=normal'; + } + + $ogTags = [ + 'Appwrite Cloud Card', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + ]; + + $fallback = \str_replace('', \implode('', $ogTags), $fallback); + } + $response->html($fallback); }); diff --git a/app/init.php b/app/init.php index 4ab695e505..34042d40c5 100644 --- a/app/init.php +++ b/app/init.php @@ -1028,6 +1028,11 @@ App::setResource('console', function () { ], 'authWhitelistEmails' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [], 'authWhitelistIPs' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null)) : [], + 'authProviders' => [ + 'githubEnabled' => true, + 'githubSecret' => App::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''), + 'githubAppid' => App::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '') + ], ]); }, []); @@ -1174,3 +1179,21 @@ App::setResource('servers', function () { return $languages; }); + +App::setResource('contributors', function () { + $path = 'app/config/cloud/contributors.json'; + $list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; + return $list; +}, []); + +App::setResource('employees', function () { + $path = 'app/config/cloud/employees.json'; + $list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; + return $list; +}, []); + +App::setResource('heroes', function () { + $path = 'app/config/cloud/heroes.json'; + $list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; + return $list; +}, []); diff --git a/app/views/general/error.phtml b/app/views/general/error.phtml index d5a11246f7..744b0acf75 100644 --- a/app/views/general/error.phtml +++ b/app/views/general/error.phtml @@ -17,49 +17,96 @@ $title = $this->getParam('title', '') <?php echo $title; ?> + + + + + + - + +
+
+
+

Error

+

+ Error ID: + +
+

Back to

+ -
-

Error

- -

- - Error ID: - - -
- -

Back to

- - - -
- -

Error Trace

- - - - $value) : ?> - - - - + +
+

Error Trace

+ +
- - - - - -
+ $value) : ?> + + + + + +
+ + + + + +
- - -
- +
+ + + +
+
+ Powered by + + + + + + + + +
+
+ + \ No newline at end of file diff --git a/app/workers/builds.php b/app/workers/builds.php index 73c1fe2a89..5c395fa6ba 100644 --- a/app/workers/builds.php +++ b/app/workers/builds.php @@ -219,8 +219,6 @@ class BuildsV1 extends Worker Console::success("Build id: $buildId created"); - $function->setAttribute('scheduleUpdatedAt', DateTime::now()); - /** Set auto deploy */ if ($deployment->getAttribute('activate') === true) { $function->setAttribute('deploymentInternalId', $deployment->getInternalId()); @@ -231,7 +229,7 @@ class BuildsV1 extends Worker /** Update function schedule */ $dbForConsole = $this->getConsoleDB(); $schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId')); - $schedule->setAttribute('resourceUpdatedAt', $function->getAttribute('scheduleUpdatedAt')); + $schedule->setAttribute('resourceUpdatedAt', DateTime::now()); $schedule ->setAttribute('schedule', $function->getAttribute('schedule')) diff --git a/bin/clear-card-cache b/bin/clear-card-cache new file mode 100644 index 0000000000..b39bc13651 --- /dev/null +++ b/bin/clear-card-cache @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/src/code/app/cli.php clear-card-cache $@ \ No newline at end of file diff --git a/bin/patch-create-missing-schedules b/bin/patch-create-missing-schedules deleted file mode 100644 index e38d3e9a6f..0000000000 --- a/bin/patch-create-missing-schedules +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -php /usr/src/code/app/cli.php patch-create-missing-schedules $@ \ No newline at end of file diff --git a/bin/patch-delete-schedule-updated-at-attribute b/bin/patch-delete-schedule-updated-at-attribute new file mode 100644 index 0000000000..3e28289cbe --- /dev/null +++ b/bin/patch-delete-schedule-updated-at-attribute @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/src/code/app/cli.php patch-delete-schedule-updated-at-attribute $@ \ No newline at end of file diff --git a/composer.json b/composer.json index bd7be22322..a767b55df8 100644 --- a/composer.json +++ b/composer.json @@ -44,10 +44,10 @@ "appwrite/php-clamav": "1.1.*", "appwrite/php-runtimes": "0.11.*", "utopia-php/abuse": "0.16.*", - "utopia-php/analytics": "0.2.*", + "utopia-php/analytics": "0.10.2", "utopia-php/audit": "0.17.*", "utopia-php/cache": "0.8.*", - "utopia-php/cli": "0.14.*", + "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", "utopia-php/database": "0.28.*", "utopia-php/queue": "0.5.*", @@ -73,8 +73,7 @@ "phpmailer/phpmailer": "6.6.0", "chillerlan/php-qrcode": "4.3.3", "adhocore/jwt": "1.1.2", - "slickdeals/statsd": "3.1.0", - "league/csv": "^9.0.0" + "slickdeals/statsd": "3.1.0" }, "repositories": [ { diff --git a/docker-compose.yml b/docker-compose.yml index 65512eb2c5..79fb578632 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -145,7 +145,6 @@ services: - _APP_SMTP_SECURE - _APP_SMTP_USERNAME - _APP_SMTP_PASSWORD - - _APP_HAMSTER_RECIPIENTS - _APP_USAGE_STATS - _APP_INFLUXDB_HOST - _APP_INFLUXDB_PORT @@ -176,6 +175,8 @@ services: - _APP_SMS_PROVIDER - _APP_SMS_FROM - _APP_REGION + - _APP_CONSOLE_GITHUB_APP_ID + - _APP_CONSOLE_GITHUB_SECRET appwrite-realtime: entrypoint: realtime @@ -585,8 +586,9 @@ services: - _APP_CONNECTIONS_DB_CONSOLE - _APP_CONNECTIONS_DB_PROJECT - _APP_CONNECTIONS_CACHE - - _APP_HAMSTER_RECIPIENTS - _APP_HAMSTER_INTERVAL + - _APP_HAMSTER_TIME + - _APP_MIXPANEL_TOKEN appwrite-maintenance: entrypoint: maintenance diff --git a/public/fonts/Inter-Bold.ttf b/public/fonts/Inter-Bold.ttf new file mode 100644 index 0000000000..8e82c70d10 Binary files /dev/null and b/public/fonts/Inter-Bold.ttf differ diff --git a/public/fonts/Inter-Medium.ttf b/public/fonts/Inter-Medium.ttf new file mode 100644 index 0000000000..9a3396fc43 Binary files /dev/null and b/public/fonts/Inter-Medium.ttf differ diff --git a/public/fonts/Inter-Regular.ttf b/public/fonts/Inter-Regular.ttf new file mode 100644 index 0000000000..8d4eebf206 Binary files /dev/null and b/public/fonts/Inter-Regular.ttf differ diff --git a/public/fonts/Inter-SemiBold.ttf b/public/fonts/Inter-SemiBold.ttf new file mode 100644 index 0000000000..c6aeeb16a6 Binary files /dev/null and b/public/fonts/Inter-SemiBold.ttf differ diff --git a/public/fonts/Poppins-Bold.ttf b/public/fonts/Poppins-Bold.ttf new file mode 100644 index 0000000000..00559eeb29 Binary files /dev/null and b/public/fonts/Poppins-Bold.ttf differ diff --git a/public/fonts/SourceCodePro-Regular.ttf b/public/fonts/SourceCodePro-Regular.ttf new file mode 100644 index 0000000000..c37a7b78d0 Binary files /dev/null and b/public/fonts/SourceCodePro-Regular.ttf differ diff --git a/public/images/cards/cloud/back-golden.png b/public/images/cards/cloud/back-golden.png new file mode 100644 index 0000000000..68f1114c45 Binary files /dev/null and b/public/images/cards/cloud/back-golden.png differ diff --git a/public/images/cards/cloud/back-platinum.png b/public/images/cards/cloud/back-platinum.png new file mode 100644 index 0000000000..cd56d227a2 Binary files /dev/null and b/public/images/cards/cloud/back-platinum.png differ diff --git a/public/images/cards/cloud/back.png b/public/images/cards/cloud/back.png new file mode 100644 index 0000000000..bb8e5b0b29 Binary files /dev/null and b/public/images/cards/cloud/back.png differ diff --git a/public/images/cards/cloud/contributor-skew.png b/public/images/cards/cloud/contributor-skew.png new file mode 100644 index 0000000000..c80b40990e Binary files /dev/null and b/public/images/cards/cloud/contributor-skew.png differ diff --git a/public/images/cards/cloud/contributor.png b/public/images/cards/cloud/contributor.png new file mode 100644 index 0000000000..203f0f18e8 Binary files /dev/null and b/public/images/cards/cloud/contributor.png differ diff --git a/public/images/cards/cloud/employee-skew.png b/public/images/cards/cloud/employee-skew.png new file mode 100644 index 0000000000..487b50c1d1 Binary files /dev/null and b/public/images/cards/cloud/employee-skew.png differ diff --git a/public/images/cards/cloud/employee.png b/public/images/cards/cloud/employee.png new file mode 100644 index 0000000000..587c1feb84 Binary files /dev/null and b/public/images/cards/cloud/employee.png differ diff --git a/public/images/cards/cloud/front-golden.png b/public/images/cards/cloud/front-golden.png new file mode 100644 index 0000000000..6f7c251de4 Binary files /dev/null and b/public/images/cards/cloud/front-golden.png differ diff --git a/public/images/cards/cloud/front-platinum.png b/public/images/cards/cloud/front-platinum.png new file mode 100644 index 0000000000..0bea38e9f3 Binary files /dev/null and b/public/images/cards/cloud/front-platinum.png differ diff --git a/public/images/cards/cloud/front.png b/public/images/cards/cloud/front.png new file mode 100644 index 0000000000..19dcad74b7 Binary files /dev/null and b/public/images/cards/cloud/front.png differ diff --git a/public/images/cards/cloud/github-skew.png b/public/images/cards/cloud/github-skew.png new file mode 100644 index 0000000000..8f836f9bd5 Binary files /dev/null and b/public/images/cards/cloud/github-skew.png differ diff --git a/public/images/cards/cloud/github.png b/public/images/cards/cloud/github.png new file mode 100644 index 0000000000..b0ba724d40 Binary files /dev/null and b/public/images/cards/cloud/github.png differ diff --git a/public/images/cards/cloud/hero-skew.png b/public/images/cards/cloud/hero-skew.png new file mode 100644 index 0000000000..3928f66356 Binary files /dev/null and b/public/images/cards/cloud/hero-skew.png differ diff --git a/public/images/cards/cloud/hero.png b/public/images/cards/cloud/hero.png new file mode 100644 index 0000000000..811df0f8cd Binary files /dev/null and b/public/images/cards/cloud/hero.png differ diff --git a/public/images/cards/cloud/og-background-logo.png b/public/images/cards/cloud/og-background-logo.png new file mode 100644 index 0000000000..c426c89171 Binary files /dev/null and b/public/images/cards/cloud/og-background-logo.png differ diff --git a/public/images/cards/cloud/og-background1.png b/public/images/cards/cloud/og-background1.png new file mode 100644 index 0000000000..cbd7221feb Binary files /dev/null and b/public/images/cards/cloud/og-background1.png differ diff --git a/public/images/cards/cloud/og-background2.png b/public/images/cards/cloud/og-background2.png new file mode 100644 index 0000000000..f62d46f8e3 Binary files /dev/null and b/public/images/cards/cloud/og-background2.png differ diff --git a/public/images/cards/cloud/og-background3.png b/public/images/cards/cloud/og-background3.png new file mode 100644 index 0000000000..3746cb620d Binary files /dev/null and b/public/images/cards/cloud/og-background3.png differ diff --git a/public/images/cards/cloud/og-card-golden1.png b/public/images/cards/cloud/og-card-golden1.png new file mode 100644 index 0000000000..563d64f98c Binary files /dev/null and b/public/images/cards/cloud/og-card-golden1.png differ diff --git a/public/images/cards/cloud/og-card-golden2.png b/public/images/cards/cloud/og-card-golden2.png new file mode 100644 index 0000000000..70331f1fcd Binary files /dev/null and b/public/images/cards/cloud/og-card-golden2.png differ diff --git a/public/images/cards/cloud/og-card-golden3.png b/public/images/cards/cloud/og-card-golden3.png new file mode 100644 index 0000000000..f03ee1d265 Binary files /dev/null and b/public/images/cards/cloud/og-card-golden3.png differ diff --git a/public/images/cards/cloud/og-card-platinum1.png b/public/images/cards/cloud/og-card-platinum1.png new file mode 100644 index 0000000000..736ae50afe Binary files /dev/null and b/public/images/cards/cloud/og-card-platinum1.png differ diff --git a/public/images/cards/cloud/og-card-platinum2.png b/public/images/cards/cloud/og-card-platinum2.png new file mode 100644 index 0000000000..bfc975c40c Binary files /dev/null and b/public/images/cards/cloud/og-card-platinum2.png differ diff --git a/public/images/cards/cloud/og-card-platinum3.png b/public/images/cards/cloud/og-card-platinum3.png new file mode 100644 index 0000000000..4f08ffcd07 Binary files /dev/null and b/public/images/cards/cloud/og-card-platinum3.png differ diff --git a/public/images/cards/cloud/og-card1.png b/public/images/cards/cloud/og-card1.png new file mode 100644 index 0000000000..8cf9b7fd15 Binary files /dev/null and b/public/images/cards/cloud/og-card1.png differ diff --git a/public/images/cards/cloud/og-card2.png b/public/images/cards/cloud/og-card2.png new file mode 100644 index 0000000000..eb5ad1d80b Binary files /dev/null and b/public/images/cards/cloud/og-card2.png differ diff --git a/public/images/cards/cloud/og-card3.png b/public/images/cards/cloud/og-card3.png new file mode 100644 index 0000000000..1766b24d94 Binary files /dev/null and b/public/images/cards/cloud/og-card3.png differ diff --git a/public/images/cards/cloud/og-shadow-golden.png b/public/images/cards/cloud/og-shadow-golden.png new file mode 100644 index 0000000000..1421d03a29 Binary files /dev/null and b/public/images/cards/cloud/og-shadow-golden.png differ diff --git a/public/images/cards/cloud/og-shadow-platinum.png b/public/images/cards/cloud/og-shadow-platinum.png new file mode 100644 index 0000000000..29d8404dc4 Binary files /dev/null and b/public/images/cards/cloud/og-shadow-platinum.png differ diff --git a/public/images/cards/cloud/og-shadow.png b/public/images/cards/cloud/og-shadow.png new file mode 100644 index 0000000000..ccb005c581 Binary files /dev/null and b/public/images/cards/cloud/og-shadow.png differ diff --git a/src/Appwrite/Auth/OAuth2/Github.php b/src/Appwrite/Auth/OAuth2/Github.php index c582617d67..8b9208fc06 100644 --- a/src/Appwrite/Auth/OAuth2/Github.php +++ b/src/Appwrite/Auth/OAuth2/Github.php @@ -158,6 +158,18 @@ class Github extends OAuth2 return $user['name'] ?? ''; } + /** + * @param string $accessToken + * + * @return string + */ + public function getUserSlug(string $accessToken): string + { + $user = $this->getUser($accessToken); + + return $user['login'] ?? ''; + } + /** * @param string $accessToken * @@ -171,13 +183,27 @@ class Github extends OAuth2 $emails = $this->request('GET', 'https://api.github.com/user/emails', ['Authorization: token ' . \urlencode($accessToken)]); $emails = \json_decode($emails, true); + + $verifiedEmail = null; + $primaryEmail = null; + foreach ($emails as $email) { if (isset($email['verified']) && $email['verified'] === true) { - $this->user['email'] = $email['email']; - $this->user['verified'] = $email['verified']; - break; + $verifiedEmail = $email; + + if (isset($email['primary']) && $email['primary'] === true) { + $primaryEmail = $email; + } } } + + if (!empty($primaryEmail)) { + $this->user['email'] = $primaryEmail['email']; + $this->user['verified'] = $primaryEmail['verified']; + } elseif (!empty($verifiedEmail)) { + $this->user['email'] = $verifiedEmail['email']; + $this->user['verified'] = $verifiedEmail['verified']; + } } return $this->user; diff --git a/src/Appwrite/Platform/Services/Tasks.php b/src/Appwrite/Platform/Services/Tasks.php index 00cf3f89c5..b82dca422f 100644 --- a/src/Appwrite/Platform/Services/Tasks.php +++ b/src/Appwrite/Platform/Services/Tasks.php @@ -13,6 +13,8 @@ use Appwrite\Platform\Tasks\SDKs; use Appwrite\Platform\Tasks\Specs; use Appwrite\Platform\Tasks\SSL; use Appwrite\Platform\Tasks\Hamster; +use Appwrite\Platform\Tasks\PatchDeleteScheduleUpdatedAtAttribute; +use Appwrite\Platform\Tasks\ClearCardCache; use Appwrite\Platform\Tasks\Usage; use Appwrite\Platform\Tasks\Vars; use Appwrite\Platform\Tasks\Version; @@ -33,6 +35,8 @@ class Tasks extends Service ->addAction(Install::getName(), new Install()) ->addAction(Maintenance::getName(), new Maintenance()) ->addAction(PatchCreateMissingSchedules::getName(), new PatchCreateMissingSchedules()) + ->addAction(ClearCardCache::getName(), new ClearCardCache()) + ->addAction(PatchDeleteScheduleUpdatedAtAttribute::getName(), new PatchDeleteScheduleUpdatedAtAttribute()) ->addAction(Schedule::getName(), new Schedule()) ->addAction(Migrate::getName(), new Migrate()) ->addAction(SDKs::getName(), new SDKs()) diff --git a/src/Appwrite/Platform/Tasks/ClearCardCache.php b/src/Appwrite/Platform/Tasks/ClearCardCache.php new file mode 100644 index 0000000000..d3153b995c --- /dev/null +++ b/src/Appwrite/Platform/Tasks/ClearCardCache.php @@ -0,0 +1,62 @@ +desc('Deletes card cache for specific user') + ->param('userId', '', new UID(), 'User UID.', false) + ->inject('dbForConsole') + ->callback(fn (string $userId, Database $dbForConsole) => $this->action($userId, $dbForConsole)); + } + + public function action(string $userId, Database $dbForConsole): void + { + Authorization::disable(); + Authorization::setDefaultStatus(false); + + Console::title('ClearCardCache V1'); + Console::success(APP_NAME . ' ClearCardCache v1 has started'); + $resources = ['card/' . $userId, 'card-back/' . $userId, 'card-og/' . $userId]; + + $caches = Authorization::skip(fn () => $dbForConsole->find('cache', [ + Query::equal('resource', $resources), + Query::limit(100) + ])); + + $count = \count($caches); + Console::info("Going to delete {$count} cache records in 10 seconds..."); + \sleep(10); + + foreach ($caches as $cache) { + $key = $cache->getId(); + + $cacheFolder = new Cache( + new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-console') + ); + + $cacheFolder->purge($key); + + Authorization::skip(fn () => $dbForConsole->deleteDocument('cache', $cache->getId())); + } + + Console::success(APP_NAME . ' ClearCardCache v1 has finished'); + } +} diff --git a/src/Appwrite/Platform/Tasks/Hamster.php b/src/Appwrite/Platform/Tasks/Hamster.php index 723f3e0e47..ad3db498ae 100644 --- a/src/Appwrite/Platform/Tasks/Hamster.php +++ b/src/Appwrite/Platform/Tasks/Hamster.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Tasks; +use Appwrite\Network\Validator\Origin; use Exception; use Utopia\App; use Utopia\Platform\Action; @@ -10,53 +11,36 @@ use Utopia\CLI\Console; use Utopia\Database\Database; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; -use League\Csv\Writer; -use PHPMailer\PHPMailer\PHPMailer; +use Utopia\Analytics\Adapter\Mixpanel; +use Utopia\Analytics\Event; +use Utopia\Config\Config; use Utopia\Database\Document; use Utopia\Pools\Group; -use Utopia\Registry\Registry; class Hamster extends Action { - private array $columns = [ - 'Project ID', - 'Project Name', - 'Functions', - 'Deployments', - 'Members', - 'Domains', - 'Files', - 'Buckets', - 'Databases', - 'Documents', - 'Collections', - 'Storage', - 'Requests', - 'Bandwidth', - 'Users', - 'Sessions', - 'Executions' - ]; - - private array $usageStats = [ - 'Files' => 'files.$all.count.total', - 'Buckets' => 'buckets.$all.count.total', - 'Databases' => 'databases.$all.count.total', - 'Documents' => 'documents.$all.count.total', - 'Collections' => 'collections.$all.count.total', - 'Storage' => 'project.$all.storage.size', - 'Requests' => 'project.$all.network.requests', - 'Bandwidth' => 'project.$all.network.bandwidth', - 'Users' => 'users.$all.count.total', - 'Sessions' => 'sessions.$all.requests.create', - 'Executions' => 'executions.$all.compute.total', + private array $metrics = [ + 'usage_files' => 'files.$all.count.total', + 'usage_buckets' => 'buckets.$all.count.total', + 'usage_databases' => 'databases.$all.count.total', + 'usage_documents' => 'documents.$all.count.total', + 'usage_collections' => 'collections.$all.count.total', + 'usage_storage' => 'project.$all.storage.size', + 'usage_requests' => 'project.$all.network.requests', + 'usage_bandwidth' => 'project.$all.network.bandwidth', + 'usage_users' => 'users.$all.count.total', + 'usage_sessions' => 'sessions.email.requests.create', + 'usage_executions' => 'executions.$all.compute.total', ]; protected string $directory = '/usr/local'; + protected string $path; protected string $date; + protected Mixpanel $mixpanel; + public static function getName(): string { return 'hamster'; @@ -64,166 +48,254 @@ class Hamster extends Action public function __construct() { + $this->mixpanel = new Mixpanel(App::getEnv('_APP_MIXPANEL_TOKEN', '')); + $this ->desc('Get stats for projects') - ->inject('register') ->inject('pools') ->inject('cache') ->inject('dbForConsole') - ->callback(function (Registry $register, Group $pools, Cache $cache, Database $dbForConsole) { - $this->action($register, $pools, $cache, $dbForConsole); + ->callback(function (Group $pools, Cache $cache, Database $dbForConsole) { + $this->action($pools, $cache, $dbForConsole); }); } - private function getStats(Database $dbForConsole, Database $dbForProject, Document $project): array + private function getStatsPerProject(Group $pools, Cache $cache, Database $dbForConsole) { - $stats = []; + $this->calculateByGroup('projects', $dbForConsole, function (Database $dbForConsole, Document $project) use ($pools, $cache) { + /** + * Skip user projects with id 'console' + */ + if ($project->getId() === 'console') { + Console::info("Skipping project console"); + return; + } - /** Get Project ID */ - $stats['Project ID'] = $project->getId(); + Console::log("Getting stats for {$project->getId()}"); - /** Get Project Name */ - $stats['Project Name'] = $project->getAttribute('name'); + try { + $db = $project->getAttribute('database'); + $adapter = $pools + ->get($db) + ->pop() + ->getResource(); - /** Get Total Functions */ - $stats['Functions'] = $dbForProject->count('functions', [], APP_LIMIT_COUNT); + $dbForProject = new Database($adapter, $cache); + $dbForProject->setDefaultDatabase('appwrite'); + $dbForProject->setNamespace('_' . $project->getInternalId()); - /** Get Total Deployments */ - $stats['Deployments'] = $dbForProject->count('deployments', [], APP_LIMIT_COUNT); + $statsPerProject = []; - /** Get Total Members */ - $teamInternalId = $project->getAttribute('teamInternalId', null); - if ($teamInternalId) { - $stats['Members'] = $dbForConsole->count('memberships', [ - Query::equal('teamInternalId', [$teamInternalId]) - ], APP_LIMIT_COUNT); - } else { - $stats['Members'] = 0; - } + $statsPerProject['time'] = microtime(true); - /** Get Domains */ - $stats['Domains'] = $dbForProject->count('domains', [], APP_LIMIT_COUNT); + /** Get Project ID */ + $statsPerProject['project_id'] = $project->getId(); - /** Get Usage stats */ - $range = '90d'; - $periods = [ - '90d' => [ - 'period' => '1d', - 'limit' => 90, - ], - ]; + /** Get project created time */ + $statsPerProject['project_created'] = $project->getAttribute('$createdAt'); - $metrics = array_values($this->usageStats); - Authorization::skip(function () use ($dbForProject, $periods, $range, $metrics, &$stats) { - foreach ($metrics as $metric) { - $limit = $periods[$range]['limit']; - $period = $periods[$range]['period']; + /** Get Project Name */ + $statsPerProject['project_name'] = $project->getAttribute('name'); - $requestDocs = $dbForProject->find('stats', [ - Query::equal('period', [$period]), - Query::equal('metric', [$metric]), - Query::limit($limit), - Query::orderDesc('time'), - ]); + /** Get Total Functions */ + $statsPerProject['custom_functions'] = $dbForProject->count('functions', [], APP_LIMIT_COUNT); - $stats[$metric] = []; - foreach ($requestDocs as $requestDoc) { - $stats[$metric][] = [ - 'value' => $requestDoc->getAttribute('value'), - 'date' => $requestDoc->getAttribute('time'), - ]; + foreach (\array_keys(Config::getParam('runtimes')) as $runtime) { + $statsPerProject['custom_functions_' . $runtime] = $dbForProject->count('functions', [ + Query::equal('runtime', [$runtime]), + ], APP_LIMIT_COUNT); } - $stats[$metric] = array_reverse($stats[$metric]); - // Calculate aggregate of each metric - $stats[$metric] = array_sum(array_column($stats[$metric], 'value')); + /** Get Total Deployments */ + $statsPerProject['custom_deployments'] = $dbForProject->count('deployments', [], APP_LIMIT_COUNT); + + /** Get Total Teams */ + $statsPerProject['custom_teams'] = $dbForProject->count('teams', [], APP_LIMIT_COUNT); + + /** Get Total Members */ + $teamInternalId = $project->getAttribute('teamInternalId', null); + if ($teamInternalId) { + $statsPerProject['custom_organization_members'] = $dbForConsole->count('memberships', [ + Query::equal('teamInternalId', [$teamInternalId]) + ], APP_LIMIT_COUNT); + } else { + $statsPerProject['custom_organization_members'] = 0; + } + + /** Get Email and Name of the project owner */ + if ($teamInternalId) { + $membership = $dbForConsole->findOne('memberships', [ + Query::equal('teamInternalId', [$teamInternalId]), + ]); + + if (!$membership || $membership->isEmpty()) { + throw new Exception('Membership not found. Skipping project : ' . $project->getId()); + } + + $userInternalId = $membership->getAttribute('userInternalId', null); + if ($userInternalId) { + $user = $dbForConsole->findOne('users', [ + Query::equal('_id', [$userInternalId]), + ]); + + $statsPerProject['email'] = $user->getAttribute('email', null); + $statsPerProject['name'] = $user->getAttribute('name', null); + } + } + + /** Get Domains */ + $statsPerProject['custom_domains'] = $dbForProject->count('domains', [], APP_LIMIT_COUNT); + + /** Get Platforms */ + $platforms = $dbForConsole->find('platforms', [ + Query::equal('projectInternalId', [$project->getInternalId()]), + Query::limit(APP_LIMIT_COUNT) + ]); + + $statsPerProject['custom_platforms_web'] = sizeof(array_filter($platforms, function ($platform) { + return $platform['type'] === 'web'; + })); + + $statsPerProject['custom_platforms_android'] = sizeof(array_filter($platforms, function ($platform) { + return $platform['type'] === 'android'; + })); + + $statsPerProject['custom_platforms_apple'] = sizeof(array_filter($platforms, function ($platform) { + return str_contains($platform['type'], 'apple'); + })); + + $statsPerProject['custom_platforms_flutter'] = sizeof(array_filter($platforms, function ($platform) { + return str_contains($platform['type'], 'flutter'); + })); + + $flutterPlatforms = [Origin::CLIENT_TYPE_FLUTTER_ANDROID, Origin::CLIENT_TYPE_FLUTTER_IOS, Origin::CLIENT_TYPE_FLUTTER_MACOS, Origin::CLIENT_TYPE_FLUTTER_WINDOWS, Origin::CLIENT_TYPE_FLUTTER_LINUX]; + + foreach ($flutterPlatforms as $flutterPlatform) { + $statsPerProject['custom_platforms_' . $flutterPlatform] = sizeof(array_filter($platforms, function ($platform) use ($flutterPlatform) { + return $platform['type'] === $flutterPlatform; + })); + } + + $statsPerProject['custom_platforms_api_keys'] = $dbForConsole->count('keys', [ + Query::equal('projectInternalId', [$project->getInternalId()]), + Query::limit(APP_LIMIT_COUNT) + ]); + + /** Get Usage $statsPerProject */ + $periods = [ + 'infinity' => [ + 'period' => '1d', + 'limit' => 90, + ], + '24h' => [ + 'period' => '1h', + 'limit' => 24, + ], + ]; + + Authorization::skip(function () use ($dbForProject, $periods, &$statsPerProject) { + foreach ($this->metrics as $key => $metric) { + foreach ($periods as $periodKey => $periodValue) { + $limit = $periodValue['limit']; + $period = $periodValue['period']; + + $requestDocs = $dbForProject->find('stats', [ + Query::equal('period', [$period]), + Query::equal('metric', [$metric]), + Query::limit($limit), + Query::orderDesc('time'), + ]); + + $statsPerProject[$key . '_' . $periodKey] = []; + foreach ($requestDocs as $requestDoc) { + $statsPerProject[$key . '_' . $periodKey][] = [ + 'value' => $requestDoc->getAttribute('value'), + 'date' => $requestDoc->getAttribute('time'), + ]; + } + + $statsPerProject[$key . '_' . $periodKey] = array_reverse($statsPerProject[$key . '_' . $periodKey]); + // Calculate aggregate of each metric + $statsPerProject[$key . '_' . $periodKey] = array_sum(array_column($statsPerProject[$key . '_' . $periodKey], 'value')); + } + } + }); + + if (isset($statsPerProject['email'])) { + /** Send data to mixpanel */ + $res = $this->mixpanel->createProfile($statsPerProject['email'], '', [ + 'name' => $statsPerProject['name'], + 'email' => $statsPerProject['email'] + ]); + + if (!$res) { + Console::error('Failed to create user profile for project: ' . $project->getId()); + } + + $event = new Event(); + $event + ->setName('Project Daily Usage') + ->setProps($statsPerProject); + $res = $this->mixpanel->createEvent($event); + if (!$res) { + Console::error('Failed to create event for project: ' . $project->getId()); + } + } + } catch (Exception $e) { + Console::error('Failed to send stats for project: ' . $project->getId()); + Console::error($e->getMessage()); + } finally { + $pools + ->get($db) + ->reclaim(); } }); - - return $stats; } - public function action(Registry $register, Group $pools, Cache $cache, Database $dbForConsole): void + public function action(Group $pools, Cache $cache, Database $dbForConsole): void { Console::title('Cloud Hamster V1'); - Console::success(APP_NAME . ' cloud hamster process v1 has started'); + Console::success(APP_NAME . ' cloud hamster process has started'); - $interval = (int) App::getEnv('_APP_HAMSTER_INTERVAL', '30'); // 30 seconds (by default) + $sleep = (int) App::getEnv('_APP_HAMSTER_INTERVAL', '30'); // 30 seconds (by default) - Console::loop(function () use ($register, $pools, $cache, $dbForConsole, $interval) { + $jobInitTime = App::getEnv('_APP_HAMSTER_TIME', '22:00'); // (hour:minutes) + $now = new \DateTime(); + $now->setTimezone(new \DateTimeZone(date_default_timezone_get())); + $next = new \DateTime($now->format("Y-m-d $jobInitTime")); + $next->setTimezone(new \DateTimeZone(date_default_timezone_get())); + $delay = $next->getTimestamp() - $now->getTimestamp(); + /** + * If time passed for the target day. + */ + if ($delay <= 0) { + $next->add(\DateInterval::createFromDateString('1 days')); + $delay = $next->getTimestamp() - $now->getTimestamp(); + } + + Console::log('[' . $now->format("Y-m-d H:i:s.v") . '] Delaying for ' . $delay . ' setting loop to [' . $next->format("Y-m-d H:i:s.v") . ']'); + + Console::loop(function () use ($pools, $cache, $dbForConsole, $sleep) { $now = date('d-m-Y H:i:s', time()); - Console::info("[{$now}] Getting Cloud Usage Stats every {$interval} seconds"); + Console::info("[{$now}] Getting Cloud Usage Stats every {$sleep} seconds"); $loopStart = microtime(true); /* Initialise new Utopia app */ $app = new App('UTC'); - $console = $app->getResource('console'); - /** CSV stuff */ - $this->date = date('Y-m-d'); - $this->path = "{$this->directory}/stats_{$this->date}.csv"; - $csv = Writer::createFromPath($this->path, 'w'); - $csv->insertOne($this->columns); + Console::info('Getting stats for all projects'); + $this->getStatsPerProject($pools, $cache, $dbForConsole); + Console::success('Completed getting stats for all projects'); - /** Database connections */ - $totalProjects = $dbForConsole->count('projects') + 1; - Console::success("Found a total of: {$totalProjects} projects"); + Console::info('Getting stats for all organizations'); + $this->getStatsPerOrganization($dbForConsole); + Console::success('Completed getting stats for all organizations'); - $projects = [$console]; - $count = 0; - $limit = 30; - $sum = 30; - $offset = 0; - while (!empty($projects)) { - foreach ($projects as $project) { - /** - * Skip user projects with id 'console' - */ - if ($project->getId() === 'console') { - continue; - } - - Console::info("Getting stats for {$project->getId()}"); - - try { - $db = $project->getAttribute('database'); - $adapter = $pools - ->get($db) - ->pop() - ->getResource(); - - $dbForProject = new Database($adapter, $cache); - $dbForProject->setDefaultDatabase('appwrite'); - $dbForProject->setNamespace('_' . $project->getInternalId()); - - $statsPerProject = $this->getStats($dbForConsole, $dbForProject, $project); - $csv->insertOne(array_values($statsPerProject)); - } catch (\Throwable $th) { - throw $th; - Console::error('Failed to update project ("' . $project->getId() . '") version with error: ' . $th->getMessage()); - } finally { - $pools - ->get($db) - ->reclaim(); - } - } - - $sum = \count($projects); - - $projects = $dbForConsole->find('projects', [ - Query::limit($limit), - Query::offset($offset), - ]); - - $offset = $offset + $limit; - $count = $count + $sum; - - Console::log('Iterated through ' . $count . '/' . $totalProjects . ' projects...'); - } - - $this->sendEmail($register); + Console::info('Getting stats for all users'); + $this->getStatsPerUser($dbForConsole); + Console::success('Completed getting stats for all users'); $pools ->get('console') @@ -232,40 +304,138 @@ class Hamster extends Action $loopTook = microtime(true) - $loopStart; $now = date('d-m-Y H:i:s', time()); Console::info("[{$now}] Cloud Stats took {$loopTook} seconds"); - }, $interval); + }, $sleep, $delay); } - private function sendEmail(Registry $register) + protected function calculateByGroup(string $collection, Database $dbForConsole, callable $callback) { - /** @var \PHPMailer\PHPMailer\PHPMailer $mail */ - $mail = $register->get('smtp'); + $count = 0; + $chunk = 0; + $limit = 50; + $results = []; + $sum = $limit; - $mail->clearAddresses(); - $mail->clearAllRecipients(); - $mail->clearReplyTos(); - $mail->clearAttachments(); - $mail->clearBCCs(); - $mail->clearCCs(); + $executionStart = \microtime(true); - try { - /** Addresses */ - $mail->setFrom(App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), 'Appwrite Cloud Hamster'); - $recipients = explode(',', App::getEnv('_APP_HAMSTER_RECIPIENTS', '')); - foreach ($recipients as $recipient) { - $mail->addAddress($recipient); + while ($sum === $limit) { + $chunk++; + + $results = $dbForConsole->find($collection, \array_merge([ + Query::limit($limit), + Query::offset($count) + ])); + + $sum = count($results); + + Console::log('Processing chunk #' . $chunk . '. Found ' . $sum . ' documents'); + + foreach ($results as $document) { + call_user_func($callback, $dbForConsole, $document); + $count++; } - - /** Attachments */ - $mail->addAttachment($this->path); - - /** Content */ - $mail->Subject = "Cloud Report for {$this->date}"; - $mail->Body = "Please find the daily cloud report atttached"; - - $mail->send(); - Console::success('Email has been sent!'); - } catch (Exception $e) { - Console::error("Message could not be sent. Mailer Error: {$mail->ErrorInfo}"); } + + $executionEnd = \microtime(true); + + Console::log("Processed {$count} document by group in " . ($executionEnd - $executionStart) . " seconds"); + } + + protected function getStatsPerOrganization(Database $dbForConsole) + { + + $this->calculateByGroup('teams', $dbForConsole, function (Database $dbForConsole, Document $document) { + try { + $statsPerOrganization = []; + + /** Organization name */ + $statsPerOrganization['name'] = $document->getAttribute('name'); + + /** Get Email and of the organization owner */ + $membership = $dbForConsole->findOne('memberships', [ + Query::equal('teamInternalId', [$document->getInternalId()]), + ]); + + if (!$membership || $membership->isEmpty()) { + throw new Exception('Membership not found. Skipping organization : ' . $document->getId()); + } + + $userInternalId = $membership->getAttribute('userInternalId', null); + if ($userInternalId) { + $user = $dbForConsole->findOne('users', [ + Query::equal('_id', [$userInternalId]), + ]); + + $statsPerOrganization['email'] = $user->getAttribute('email', null); + } + + /** Organization Creation Date */ + $statsPerOrganization['created'] = $document->getAttribute('$createdAt'); + + /** Number of team members */ + $statsPerOrganization['members'] = $document->getAttribute('total'); + + /** Number of projects in this organization */ + $statsPerOrganization['projects'] = $dbForConsole->count('projects', [ + Query::equal('teamId', [$document->getId()]), + Query::limit(APP_LIMIT_COUNT) + ]); + + if (!isset($statsPerOrganization['email'])) { + throw new Exception('Email not found. Skipping organization : ' . $document->getId()); + } + + $event = new Event(); + $event + ->setName('Organization Daily Usage') + ->setProps($statsPerOrganization); + $res = $this->mixpanel->createEvent($event); + if (!$res) { + throw new Exception('Failed to create event for organization : ' . $document->getId()); + } + } catch (Exception $e) { + Console::error($e->getMessage()); + } + }); + } + + protected function getStatsPerUser(Database $dbForConsole) + { + $this->calculateByGroup('users', $dbForConsole, function (Database $dbForConsole, Document $document) { + try { + $statsPerUser = []; + + /** Organization name */ + $statsPerUser['name'] = $document->getAttribute('name'); + + /** Organization ID (needs to be stored as an email since mixpanel uses the email attribute as a distinctID) */ + $statsPerUser['email'] = $document->getAttribute('email'); + + /** Organization Creation Date */ + $statsPerUser['created'] = $document->getAttribute('$createdAt'); + + /** Number of teams this user is a part of */ + $statsPerUser['memberships'] = $dbForConsole->count('memberships', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_COUNT) + ]); + + if (!isset($statsPerUser['email'])) { + throw new Exception('User has no email: ' . $document->getId()); + } + + /** Send data to mixpanel */ + $event = new Event(); + $event + ->setName('User Daily Usage') + ->setProps($statsPerUser); + $res = $this->mixpanel->createEvent($event); + + if (!$res) { + throw new Exception('Failed to create user profile for user: ' . $document->getId()); + } + } catch (Exception $e) { + Console::error($e->getMessage()); + } + }); } } diff --git a/src/Appwrite/Platform/Tasks/PatchDeleteScheduleUpdatedAtAttribute.php b/src/Appwrite/Platform/Tasks/PatchDeleteScheduleUpdatedAtAttribute.php new file mode 100644 index 0000000000..95a7c4ffe1 --- /dev/null +++ b/src/Appwrite/Platform/Tasks/PatchDeleteScheduleUpdatedAtAttribute.php @@ -0,0 +1,74 @@ +desc('Ensure function collections do not have scheduleUpdatedAt attribute') + ->inject('pools') + ->inject('dbForConsole') + ->inject('getProjectDB') + ->callback(fn (Group $pools, Database $dbForConsole, callable $getProjectDB) => $this->action($pools, $dbForConsole, $getProjectDB)); + } + + /** + * Iterate over every function on every project to make sure there is a schedule. If not, recreate the schedule. + */ + public function action(Group $pools, Database $dbForConsole, callable $getProjectDB): void + { + Authorization::disable(); + Authorization::setDefaultStatus(false); + + Console::title('PatchDeleteScheduleUpdatedAtAttribute V1'); + Console::success(APP_NAME . ' PatchDeleteScheduleUpdatedAtAttribute v1 has started'); + + $limit = 100; + $projectCursor = null; + while (true) { + $projectsQueries = [Query::limit($limit)]; + if ($projectCursor !== null) { + $projectsQueries[] = Query::cursorAfter($projectCursor); + } + $projects = $dbForConsole->find('projects', $projectsQueries); + + if (count($projects) === 0) { + break; + } + + foreach ($projects as $project) { + Console::log("Checking Project " . $project->getAttribute('name') . " (" . $project->getId() . ")"); + $dbForProject = $getProjectDB($project); + + try { + /** + * Delete 'scheduleUpdatedAt' attribute + */ + $dbForProject->deleteAttribute('functions', 'scheduleUpdatedAt'); + $dbForProject->deleteCachedCollection('functions'); + Console::success("'scheduleUpdatedAt' deleted."); + } catch (\Throwable $th) { + Console::warning("'scheduleUpdatedAt' errored: {$th->getMessage()}"); + } + + $pools->reclaim(); + } + + $projectCursor = $projects[array_key_last($projects)]; + } + } +}