diff --git a/app/Models/CustomEmoji.php b/app/Models/CustomEmoji.php new file mode 100644 index 000000000..0903d5375 --- /dev/null +++ b/app/Models/CustomEmoji.php @@ -0,0 +1,41 @@ +matchAll(self::SCAN_RE) + ->map(function($match) { + $tag = Cache::remember(self::CACHE_KEY . $match, 14400, function() use($match) { + return self::whereShortcode(':' . $match . ':')->first(); + }); + + if($tag) { + $url = url('/storage/' . $tag->media_path); + return [ + 'shortcode' => $match, + 'url' => $url, + 'static_path' => $url, + 'visible_in_picker' => $tag->disabled == false + ]; + } + }) + ->filter(function($tag) { + return $tag && isset($tag['static_path']); + }) + ->values(); + } +} diff --git a/app/Services/CustomEmojiService.php b/app/Services/CustomEmojiService.php new file mode 100644 index 000000000..1a64784bb --- /dev/null +++ b/app/Services/CustomEmojiService.php @@ -0,0 +1,91 @@ +first(); + } + + public static function getByUrl($url) + { + if(Helpers::validateUrl($url) == false) { + return; + } + + $emoji = CustomEmoji::whereUri($url)->first(); + if($emoji) { + return $emoji; + } + + $res = Http::acceptJson()->get($url); + + if($res->successful()) { + $json = $res->json(); + + if( + !$json || + !isset($json['id']) || + !isset($json['type']) || + $json['type'] !== 'Emoji' || + !isset($json['icon']) || + !isset($json['icon']['mediaType']) || + !isset($json['icon']['url']) || + !isset($json['icon']['type']) || + $json['icon']['type'] !== 'Image' || + !in_array($json['icon']['mediaType'], ['image/jpeg', 'image/png', 'image/jpg']) + ) { + return; + } + + if(!self::headCheck($json['icon']['url'])) { + return; + } + $emoji = new CustomEmoji; + $emoji->shortcode = $json['name']; + $emoji->uri = $json['id']; + $emoji->domain = parse_url($json['id'], PHP_URL_HOST); + $emoji->image_remote_url = $json['icon']['url']; + $emoji->save(); + + $ext = '.' . last(explode('/', $json['icon']['mediaType'])); + $dest = storage_path('app/public/emoji/') . $emoji->id . $ext; + copy($emoji->image_remote_url, $dest); + $emoji->media_path = 'emoji/' . $emoji->id . $ext; + $emoji->save(); + + return $emoji; + } else { + return; + } + } + + public static function headCheck($url) + { + $res = Http::head($url); + + if(!$res->successful()) { + return false; + } + + $type = $res->header('content-type'); + $length = $res->header('content-length'); + + if( + !$type || + !$length || + !in_array($type, ['image/jpeg', 'image/png', 'image/jpg']) || + $length > config('federation.custom_emoji.max_size') + ) { + return false; + } + + return true; + } +} diff --git a/app/Transformer/Api/StatusStatelessTransformer.php b/app/Transformer/Api/StatusStatelessTransformer.php index 91ed1484b..8f56bf75b 100644 --- a/app/Transformer/Api/StatusStatelessTransformer.php +++ b/app/Transformer/Api/StatusStatelessTransformer.php @@ -14,6 +14,7 @@ use App\Services\StatusLabelService; use App\Services\StatusMentionService; use App\Services\ProfileService; use App\Services\PollService; +use App\Models\CustomEmoji; class StatusStatelessTransformer extends Fractal\TransformerAbstract { @@ -25,17 +26,18 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract return [ '_v' => 1, 'id' => (string) $status->id, + //'gid' => $status->group_id ? (string) $status->group_id : null, 'shortcode' => HashidService::encode($status->id), 'uri' => $status->url(), 'url' => $status->url(), - 'in_reply_to_id' => (string) $status->in_reply_to_id, - 'in_reply_to_account_id' => (string) $status->in_reply_to_profile_id, + 'in_reply_to_id' => $status->in_reply_to_id ? (string) $status->in_reply_to_id : null, + 'in_reply_to_account_id' => $status->in_reply_to_profile_id ? (string) $status->in_reply_to_profile_id : null, 'reblog' => null, 'content' => $status->rendered ?? $status->caption, 'content_text' => $status->caption, 'created_at' => $status->created_at->format('c'), - 'emojis' => [], - 'reblogs_count' => 0, + 'emojis' => CustomEmoji::scan($status->caption), + 'reblogs_count' => $status->reblogs_count ?? 0, 'favourites_count' => $status->likes_count ?? 0, 'reblogged' => null, 'favourited' => null, diff --git a/app/Util/Lexer/RestrictedNames.php b/app/Util/Lexer/RestrictedNames.php index 059f5bf00..6795870b5 100644 --- a/app/Util/Lexer/RestrictedNames.php +++ b/app/Util/Lexer/RestrictedNames.php @@ -157,6 +157,8 @@ class RestrictedNames 'embed', 'email', 'emails', + 'emoji', + 'emojis', 'error', 'explore', 'export', diff --git a/config/federation.php b/config/federation.php index ce2a5770e..d5f6b2d26 100644 --- a/config/federation.php +++ b/config/federation.php @@ -44,6 +44,13 @@ return [ 'enabled' => env('WEBFINGER', true) ], - 'network_timeline' => env('PF_NETWORK_TIMELINE', true) + 'network_timeline' => env('PF_NETWORK_TIMELINE', true), + + 'custom_emoji' => [ + 'enabled' => env('CUSTOM_EMOJI', false), + + // max size in bytes, default is 2mb + 'max_size' => env('CUSTOM_EMOJI_MAX_SIZE', 2000000), + ] ]; diff --git a/database/migrations/2022_01_19_025041_create_custom_emoji_table.php b/database/migrations/2022_01_19_025041_create_custom_emoji_table.php new file mode 100644 index 000000000..8c05d9bbb --- /dev/null +++ b/database/migrations/2022_01_19_025041_create_custom_emoji_table.php @@ -0,0 +1,47 @@ +id(); + $table->string('shortcode')->unique()->index(); + $table->string('media_path')->nullable(); + $table->string('domain')->nullable()->index(); + $table->boolean('disabled')->default(false)->index(); + $table->string('uri')->nullable(); + $table->string('image_remote_url')->nullable(); + $table->unsignedInteger('category_id')->nullable(); + $table->unique(['shortcode', 'domain']); + $table->timestamps(); + }); + + Schema::create('custom_emoji_categories', function (Blueprint $table) { + $table->id(); + $table->string('name')->unique()->index(); + $table->boolean('disabled')->default(false)->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('custom_emoji'); + Schema::dropIfExists('custom_emoji_categories'); + } +}