diff --git a/README.md b/README.md index 9aa363e..27bb568 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # dynacover -A PHP GD + TwitterOAuth CLI app to dynamically generate Twitter header images and upload them via the API. This enables you to build cool little tricks, like showing your latest followers or sponsors, your latest content created, a qrcode to something, a progress bar for a goal, and whatever you can think of. +A PHP GD + TwitterOAuth CLI app to dynamically generate Twitter header images and optionally upload them via the API. This enables you to build cool little tricks, like showing your latest followers or sponsors, your latest content created, a qrcode to something, a progress bar for a goal, and whatever you can think of. + +Other types of dynamic banners can also be generated. Dynacover uses [erikaheidi/gdaisy](https://github.com/erikaheidi/gdaisy) for image manipulation based on templates.

@@ -154,3 +156,19 @@ This will open up a text editor. You should include the full paths to both the ` */5 * * * * /usr/bin/php /home/erika/dynacover/dynacover cover update > /dev/null 2>&1 ``` +### Interactions Banner + +The "interactions banner" is generated based on your recent interactions and can be limited to only include mutuals (people that follows you and you follow them back). + +```shell +php dynacover generate interactions +``` + +For mutuals only, include the `--mutuals` flag: + +```shell +php dynacover generate interactions --mutuals +``` + +_Please notice that the "mutuals" version may have a limited set of results after filtering your latest interactions (~200 mentions)._ + diff --git a/app/Command/Generate/InteractionsController.php b/app/Command/Generate/InteractionsController.php new file mode 100644 index 0000000..43ef05a --- /dev/null +++ b/app/Command/Generate/InteractionsController.php @@ -0,0 +1,110 @@ + 675, + 'height' => 1200, + ]); + + //Set up BG as first element + $template->addPlaceholder('background', new ImagePlaceholder([ + 'width' => $template->width, + 'height' => $template->height, + 'pos_x' => 0, + 'pos_y' => 0, + 'image' => "app/Resources/images/interactions.png" + ])); + + $limit = 30; + $per_line = 5; + $prefix = "int"; + $avatar_size = 100; + $spacing = 25; + $line = 1; + $col = 1; + + /** @var TwitterServiceProvider $twitter */ + $twitter = $this->getApp()->twitter; + + //get own user credentials + $owner = $twitter->client->get('/account/verify_credentials'); + + //place owner avatar at the center + $avatar_path = str_replace('normal', 'bigger', $owner->profile_image_url_https); + $avatar = Storage::downloadImage($avatar_path); + $template->addPlaceholder('owner', new ImagePlaceholder([ + 'width' => $avatar_size*1.5, + 'height' => $avatar_size*1.5, + 'pos_x' => ($template->width/2) - ($avatar_size*1.5/2), + 'pos_y' => ($template->height/2) - ($avatar_size*1.5/2), + 'image' => $avatar, + 'filters' => [ "GDaisy\\Filter\\Circle" ] + ])); + + $source = new TwitterInteractionsImageSource(); + $featured = $source->getImageList($this->getApp(), $limit, ['mutuals' => $this->hasFlag('mutuals')]); + + //build the template + $linegap = ceil((count($featured) / 2) / $per_line); + $start_x = 20; + $start_y = 20; + + for ($i = 1; $i <= $limit; $i++) { + $pos_x = $start_x + ($spacing*$col) + ($avatar_size*$col - $avatar_size); + $pos_y = $start_y + ($spacing*$line) + ($avatar_size*$line - $avatar_size); + $template->addPlaceholder($prefix . $i, new ImagePlaceholder([ + 'width' => $avatar_size, + 'height' =>$avatar_size, + 'pos_x' => $pos_x, + 'pos_y' => $pos_y, + 'filters' => [ "GDaisy\\Filter\\Circle" ] + ])); + + $col++; + if ($i % $per_line == 0) { + if ($line == $linegap) { + //second block starts at line 7 + $line = 6; + } + $line++; + $col = 1; + } + } + + //Apply template elements + /** + * @var string $key + * @var PlaceholderInterface $placeholder + */ + foreach ($template->placeholders as $key => $placeholder) { + if ($placeholder instanceof ImagePlaceholder and $placeholder->image) { + $placeholder->apply($template->getResource(), ['image_file' => $placeholder->image]); + continue; + } + + if (isset($featured[$key])) { + $placeholder->apply($template->getResource(), $featured[$key]); + } + } + + $save_path = Storage::root() . 'latest_header.png'; + $template->write($save_path); + $this->getPrinter()->info("Finished generating cover at $save_path."); + + return 0; + } +} \ No newline at end of file diff --git a/app/Command/Generate/TwitterController.php b/app/Command/Generate/TwitterController.php index 3267db2..c6f5cf0 100644 --- a/app/Command/Generate/TwitterController.php +++ b/app/Command/Generate/TwitterController.php @@ -5,7 +5,7 @@ use App\ImageSource; use App\Storage; use App\Template; -use GDaisy\ImagePlaceholder; +use GDaisy\Placeholder\ImagePlaceholder; use GDaisy\PlaceholderInterface; use Minicli\Command\CommandController; diff --git a/app/ImageSource.php b/app/ImageSource.php index 769bc26..820f330 100644 --- a/app/ImageSource.php +++ b/app/ImageSource.php @@ -6,5 +6,5 @@ interface ImageSource { - public function getImageList(App $app, $limit = 5): array; + public function getImageList(App $app, int $limit = 5, array $params = []): array; } \ No newline at end of file diff --git a/app/ImageSource/GhSponsorImageSource.php b/app/ImageSource/GhSponsorImageSource.php index 84e4b34..9166753 100644 --- a/app/ImageSource/GhSponsorImageSource.php +++ b/app/ImageSource/GhSponsorImageSource.php @@ -12,7 +12,7 @@ class GhSponsorImageSource implements ImageSource { static string $prefix = "sp"; - public function getImageList(App $app, $limit = 5): array + public function getImageList(App $app, int $limit = 5, array $params = []): array { /** @var GithubServiceProvider $github */ $github = $app->github; diff --git a/app/ImageSource/TwitterFollowerImageSource.php b/app/ImageSource/TwitterFollowerImageSource.php index a075b66..a898667 100644 --- a/app/ImageSource/TwitterFollowerImageSource.php +++ b/app/ImageSource/TwitterFollowerImageSource.php @@ -11,7 +11,7 @@ class TwitterFollowerImageSource implements ImageSource { static string $prefix = "tw"; - public function getImageList(App $app, $limit = 5): array + public function getImageList(App $app, int $limit = 5, array $params = []): array { /** @var TwitterServiceProvider $twitter */ $twitter = $app->twitter; diff --git a/app/ImageSource/TwitterInteractionsImageSource.php b/app/ImageSource/TwitterInteractionsImageSource.php new file mode 100644 index 0000000..19cdde8 --- /dev/null +++ b/app/ImageSource/TwitterInteractionsImageSource.php @@ -0,0 +1,103 @@ +twitter; + $mutualsOnly = $params['mutuals'] ?? false; + + return $this->getFeaturedByMentions($twitter, $limit, self::$prefix, $mutualsOnly); + } + + public function getFeaturedByMentions(TwitterServiceProvider $twitter, $limit, $prefix = "int", $mutualsOnly = false): array + { + $response = $twitter->client->get('/statuses/mentions_timeline', [ + 'count' => 200, + 'include_entities' => false + ]); + + $interactions = []; + $users = []; + + //sort interactions by user id + count + foreach ($response as $tweet) { + $users[$tweet->user->id] = $tweet->user; + $int_count = $interactions[$tweet->user->id] ?? 0; + $interactions[$tweet->user->id] = $int_count+1; + } + arsort($interactions); + + if ($mutualsOnly) { + $users_ids = array_keys($users); + $query_chunks = array_chunk($users_ids, 100); + $validation_queries = []; + + foreach ($query_chunks as $chunk) { + $users_ids_list = implode(',', $chunk); + $validation_queries[] = $this->getRelationships($twitter, $users_ids_list); + } + + $validated = []; + foreach ($validation_queries as $relationships) { + foreach ($relationships as $relationship) { + if (isset($relationship->connections) && (count($relationship->connections) > 1)) { + $validated[$relationship->id_str] = $users[$relationship->id_str]; + } + } + } + + $users = $validated; + } + + $featured = []; + $count = 1; + + //build an array with user info + foreach ($interactions as $user_id => $interaction) { + $user = $users[$user_id] ?? null; + if ($user) { + if (!$user->profile_image_url_https) { + continue; + } + + $avatar_path = str_replace('normal', 'bigger', $user->profile_image_url_https); + $avatar = Storage::downloadImage($avatar_path); + + if ($avatar) { + $featured[$prefix . "$count"] = [ + 'screen_name' => $user->screen_name, + 'avatar' => $avatar, + 'image_file' => $avatar + ]; + + $count++; + } + } + + if ($count > $limit) { + break; + } + } + + return $featured; + } + + public function getRelationships(TwitterServiceProvider $twitter, string $lookup) + { + return $twitter->client->get("friendships/lookup", [ + "user_id" => $lookup + ]); + } + +} \ No newline at end of file diff --git a/app/Resources/images/interactions.png b/app/Resources/images/interactions.png new file mode 100644 index 0000000..3e13fe1 Binary files /dev/null and b/app/Resources/images/interactions.png differ diff --git a/composer.lock b/composer.lock index 46b86e1..30eb046 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "abraham/twitteroauth", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/abraham/twitteroauth.git", - "reference": "af6d0ba772731d4f83524fccb24281fe6149ef43" + "reference": "5a424e80a1200674451844fbaae8a0098a316a01" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/abraham/twitteroauth/zipball/af6d0ba772731d4f83524fccb24281fe6149ef43", - "reference": "af6d0ba772731d4f83524fccb24281fe6149ef43", + "url": "https://api.github.com/repos/abraham/twitteroauth/zipball/5a424e80a1200674451844fbaae8a0098a316a01", + "reference": "5a424e80a1200674451844fbaae8a0098a316a01", "shasum": "" }, "require": { @@ -29,7 +29,7 @@ "php-vcr/php-vcr": "^1", "php-vcr/phpunit-testlistener-vcr": "dev-php-8", "phpmd/phpmd": "^2", - "phpunit/phpunit": "^8", + "phpunit/phpunit": "^8 || ^9", "squizlabs/php_codesniffer": "^3" }, "type": "library", @@ -65,20 +65,20 @@ "issues": "https://github.com/abraham/twitteroauth/issues", "source": "https://github.com/abraham/twitteroauth" }, - "time": "2020-12-02T01:37:06+00:00" + "time": "2021-06-11T02:56:14+00:00" }, { "name": "composer/ca-bundle", - "version": "1.2.9", + "version": "1.2.10", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "78a0e288fdcebf92aa2318a8d3656168da6ac1a5" + "reference": "9fdb22c2e97a614657716178093cd1da90a64aa8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/78a0e288fdcebf92aa2318a8d3656168da6ac1a5", - "reference": "78a0e288fdcebf92aa2318a8d3656168da6ac1a5", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/9fdb22c2e97a614657716178093cd1da90a64aa8", + "reference": "9fdb22c2e97a614657716178093cd1da90a64aa8", "shasum": "" }, "require": { @@ -125,7 +125,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.2.9" + "source": "https://github.com/composer/ca-bundle/tree/1.2.10" }, "funding": [ { @@ -141,20 +141,20 @@ "type": "tidelift" } ], - "time": "2021-01-12T12:10:35+00:00" + "time": "2021-06-07T13:58:28+00:00" }, { "name": "erikaheidi/gdaisy", - "version": "0.1.5", + "version": "0.1.6", "source": { "type": "git", "url": "https://github.com/erikaheidi/gdaisy.git", - "reference": "7296916e1d9c3c331767eb7fe44f686e1287f408" + "reference": "2c9bdcb602b4118e6e17b588b2497c81a052dfc0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/erikaheidi/gdaisy/zipball/7296916e1d9c3c331767eb7fe44f686e1287f408", - "reference": "7296916e1d9c3c331767eb7fe44f686e1287f408", + "url": "https://api.github.com/repos/erikaheidi/gdaisy/zipball/2c9bdcb602b4118e6e17b588b2497c81a052dfc0", + "reference": "2c9bdcb602b4118e6e17b588b2497c81a052dfc0", "shasum": "" }, "require": { @@ -183,9 +183,9 @@ "description": "php-gd templating system", "support": { "issues": "https://github.com/erikaheidi/gdaisy/issues", - "source": "https://github.com/erikaheidi/gdaisy/tree/0.1.5" + "source": "https://github.com/erikaheidi/gdaisy/tree/0.1.6" }, - "time": "2021-06-21T10:07:35+00:00" + "time": "2021-06-30T16:55:26+00:00" }, { "name": "minicli/curly",